mirror of
https://github.com/fastapi/fastapi.git
synced 2025-12-25 07:08:11 -05:00
Compare commits
4 Commits
check-tran
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
23caa2709b | ||
|
|
c264467efe | ||
|
|
2b212ddd76 | ||
|
|
7203e860b3 |
@@ -120,6 +120,12 @@ The key features are:
|
||||
|
||||
---
|
||||
|
||||
## FastAPI mini documentary
|
||||
|
||||
There's a <a href="https://www.youtube.com/watch?v=mpR8ngthqiE" class="external-link" target="_blank">FastAPI mini documentary</a> released at the end of 2025, you can watch it online:
|
||||
|
||||
<a href="https://www.youtube.com/watch?v=mpR8ngthqiE" target="_blank"><img src="https://fastapi.tiangolo.com/img/fastapi-documentary.jpg" alt="FastAPI Mini Documentary"></a>
|
||||
|
||||
## **Typer**, the FastAPI of CLIs
|
||||
|
||||
<a href="https://typer.tiangolo.com" target="_blank"><img src="https://typer.tiangolo.com/img/logo-margin/logo-margin-vector.svg" style="width: 20%;"></a>
|
||||
|
||||
@@ -48,7 +48,7 @@ Sie können die verwendeten Zeilen aus dem Docstring einer *Pfadoperation-Funkti
|
||||
|
||||
Das Hinzufügen eines `\f` (ein maskiertes „Form Feed“-Zeichen) führt dazu, dass **FastAPI** die für OpenAPI verwendete Ausgabe an dieser Stelle abschneidet.
|
||||
|
||||
Sie wird nicht in der Dokumentation angezeigt, aber andere Tools (z. B. Sphinx) können den Rest verwenden.
|
||||
Sie wird nicht in der Dokumentation angezeigt, aber andere Tools (wie z. B. Sphinx) können den Rest verwenden.
|
||||
|
||||
{* ../../docs_src/path_operation_advanced_configuration/tutorial004_py310.py hl[17:27] *}
|
||||
|
||||
@@ -153,48 +153,16 @@ Und Sie könnten dies auch tun, wenn der Datentyp im Request nicht JSON ist.
|
||||
|
||||
In der folgenden Anwendung verwenden wir beispielsweise weder die integrierte Funktionalität von FastAPI zum Extrahieren des JSON-Schemas aus Pydantic-Modellen noch die automatische Validierung für JSON. Tatsächlich deklarieren wir den Request-Content-Type als YAML und nicht als JSON:
|
||||
|
||||
//// tab | Pydantic v2
|
||||
|
||||
{* ../../docs_src/path_operation_advanced_configuration/tutorial007_py39.py hl[15:20, 22] *}
|
||||
|
||||
////
|
||||
|
||||
//// tab | Pydantic v1
|
||||
|
||||
{* ../../docs_src/path_operation_advanced_configuration/tutorial007_pv1_py39.py hl[15:20, 22] *}
|
||||
|
||||
////
|
||||
|
||||
/// info | Info
|
||||
|
||||
In Pydantic Version 1 hieß die Methode zum Abrufen des JSON-Schemas für ein Modell `Item.schema()`, in Pydantic Version 2 heißt die Methode `Item.model_json_schema()`.
|
||||
|
||||
///
|
||||
|
||||
Obwohl wir nicht die standardmäßig integrierte Funktionalität verwenden, verwenden wir dennoch ein Pydantic-Modell, um das JSON-Schema für die Daten, die wir in YAML empfangen möchten, manuell zu generieren.
|
||||
|
||||
Dann verwenden wir den Request direkt und extrahieren den Body als `bytes`. Das bedeutet, dass FastAPI nicht einmal versucht, den Request-Payload als JSON zu parsen.
|
||||
Dann verwenden wir den Request direkt und extrahieren den Body als `bytes`. Das bedeutet, dass FastAPI nicht einmal versucht, die Request-Payload als JSON zu parsen.
|
||||
|
||||
Und dann parsen wir in unserem Code diesen YAML-Inhalt direkt und verwenden dann wieder dasselbe Pydantic-Modell, um den YAML-Inhalt zu validieren:
|
||||
|
||||
//// tab | Pydantic v2
|
||||
|
||||
{* ../../docs_src/path_operation_advanced_configuration/tutorial007_py39.py hl[24:31] *}
|
||||
|
||||
////
|
||||
|
||||
//// tab | Pydantic v1
|
||||
|
||||
{* ../../docs_src/path_operation_advanced_configuration/tutorial007_pv1_py39.py hl[24:31] *}
|
||||
|
||||
////
|
||||
|
||||
/// info | Info
|
||||
|
||||
In Pydantic Version 1 war die Methode zum Parsen und Validieren eines Objekts `Item.parse_obj()`, in Pydantic Version 2 heißt die Methode `Item.model_validate()`.
|
||||
|
||||
///
|
||||
|
||||
/// tip | Tipp
|
||||
|
||||
Hier verwenden wir dasselbe Pydantic-Modell wieder.
|
||||
|
||||
@@ -60,24 +60,8 @@ Auf die gleiche Weise wie bei Pydantic-Modellen deklarieren Sie Klassenattribute
|
||||
|
||||
Sie können dieselben Validierungs-Funktionen und -Tools verwenden, die Sie für Pydantic-Modelle verwenden, z. B. verschiedene Datentypen und zusätzliche Validierungen mit `Field()`.
|
||||
|
||||
//// tab | Pydantic v2
|
||||
|
||||
{* ../../docs_src/settings/tutorial001_py39.py hl[2,5:8,11] *}
|
||||
|
||||
////
|
||||
|
||||
//// tab | Pydantic v1
|
||||
|
||||
/// info | Info
|
||||
|
||||
In Pydantic v1 würden Sie `BaseSettings` direkt von `pydantic` statt von `pydantic_settings` importieren.
|
||||
|
||||
///
|
||||
|
||||
{* ../../docs_src/settings/tutorial001_pv1_py39.py hl[2,5:8,11] *}
|
||||
|
||||
////
|
||||
|
||||
/// tip | Tipp
|
||||
|
||||
Für ein schnelles Copy-and-paste verwenden Sie nicht dieses Beispiel, sondern das letzte unten.
|
||||
@@ -215,8 +199,6 @@ APP_NAME="ChimichangApp"
|
||||
|
||||
Und dann aktualisieren Sie Ihre `config.py` mit:
|
||||
|
||||
//// tab | Pydantic v2
|
||||
|
||||
{* ../../docs_src/settings/app03_an_py39/config.py hl[9] *}
|
||||
|
||||
/// tip | Tipp
|
||||
@@ -225,26 +207,6 @@ Das Attribut `model_config` wird nur für die Pydantic-Konfiguration verwendet.
|
||||
|
||||
///
|
||||
|
||||
////
|
||||
|
||||
//// tab | Pydantic v1
|
||||
|
||||
{* ../../docs_src/settings/app03_an_py39/config_pv1.py hl[9:10] *}
|
||||
|
||||
/// tip | Tipp
|
||||
|
||||
Die Klasse `Config` wird nur für die Pydantic-Konfiguration verwendet. Weitere Informationen finden Sie unter <a href="https://docs.pydantic.dev/1.10/usage/model_config/" class="external-link" target="_blank">Pydantic Model Config</a>.
|
||||
|
||||
///
|
||||
|
||||
////
|
||||
|
||||
/// info | Info
|
||||
|
||||
In Pydantic Version 1 erfolgte die Konfiguration in einer internen Klasse `Config`, in Pydantic Version 2 erfolgt sie in einem Attribut `model_config`. Dieses Attribut akzeptiert ein <abbr title="Dictionary – Zuordnungstabelle: In anderen Sprachen auch Hash, Map, Objekt, Assoziatives Array genannt">`dict`</abbr>. Um automatische Codevervollständigung und Inline-Fehlerberichte zu erhalten, können Sie `SettingsConfigDict` importieren und verwenden, um dieses `dict` zu definieren.
|
||||
|
||||
///
|
||||
|
||||
Hier definieren wir die Konfiguration `env_file` innerhalb Ihrer Pydantic-`Settings`-Klasse und setzen den Wert auf den Dateinamen mit der dotenv-Datei, die wir verwenden möchten.
|
||||
|
||||
### Die `Settings` nur einmal laden mittels `lru_cache` { #creating-the-settings-only-once-with-lru-cache }
|
||||
|
||||
@@ -2,21 +2,23 @@
|
||||
|
||||
Wenn Sie eine ältere FastAPI-App haben, nutzen Sie möglicherweise Pydantic Version 1.
|
||||
|
||||
FastAPI unterstützt seit Version 0.100.0 sowohl Pydantic v1 als auch v2.
|
||||
FastAPI Version 0.100.0 unterstützte sowohl Pydantic v1 als auch v2. Es verwendete, was auch immer Sie installiert hatten.
|
||||
|
||||
Wenn Sie Pydantic v2 installiert hatten, wurde dieses verwendet. Wenn stattdessen Pydantic v1 installiert war, wurde jenes verwendet.
|
||||
FastAPI Version 0.119.0 führte eine teilweise Unterstützung für Pydantic v1 innerhalb von Pydantic v2 (als `pydantic.v1`) ein, um die Migration zu v2 zu erleichtern.
|
||||
|
||||
Pydantic v1 ist jetzt deprecatet und die Unterstützung dafür wird in den nächsten Versionen von FastAPI entfernt, Sie sollten also zu **Pydantic v2 migrieren**. Auf diese Weise erhalten Sie die neuesten Features, Verbesserungen und Fixes.
|
||||
FastAPI 0.126.0 entfernte die Unterstützung für Pydantic v1, während `pydantic.v1` noch eine Weile unterstützt wurde.
|
||||
|
||||
/// warning | Achtung
|
||||
|
||||
Außerdem hat das Pydantic-Team die Unterstützung für Pydantic v1 in den neuesten Python-Versionen eingestellt, beginnend mit **Python 3.14**.
|
||||
Das Pydantic-Team hat die Unterstützung für Pydantic v1 in den neuesten Python-Versionen eingestellt, beginnend mit **Python 3.14**.
|
||||
|
||||
Dies schließt `pydantic.v1` ein, das unter Python 3.14 und höher nicht mehr unterstützt wird.
|
||||
|
||||
Wenn Sie die neuesten Features von Python nutzen möchten, müssen Sie sicherstellen, dass Sie Pydantic v2 verwenden.
|
||||
|
||||
///
|
||||
|
||||
Wenn Sie eine ältere FastAPI-App mit Pydantic v1 haben, zeige ich Ihnen hier, wie Sie sie zu Pydantic v2 migrieren, und die **neuen Features in FastAPI 0.119.0**, die Ihnen bei einer schrittweisen Migration helfen.
|
||||
Wenn Sie eine ältere FastAPI-App mit Pydantic v1 haben, zeige ich Ihnen hier, wie Sie sie zu Pydantic v2 migrieren, und die **Features in FastAPI 0.119.0**, die Ihnen bei einer schrittweisen Migration helfen.
|
||||
|
||||
## Offizieller Leitfaden { #official-guide }
|
||||
|
||||
@@ -44,7 +46,7 @@ Danach können Sie die Tests ausführen und prüfen, ob alles funktioniert. Fall
|
||||
|
||||
## Pydantic v1 in v2 { #pydantic-v1-in-v2 }
|
||||
|
||||
Pydantic v2 enthält alles aus Pydantic v1 als Untermodul `pydantic.v1`.
|
||||
Pydantic v2 enthält alles aus Pydantic v1 als Untermodul `pydantic.v1`. Dies wird aber in Versionen oberhalb von Python 3.13 nicht mehr unterstützt.
|
||||
|
||||
Das bedeutet, Sie können die neueste Version von Pydantic v2 installieren und die alten Pydantic‑v1‑Komponenten aus diesem Untermodul importieren und verwenden, als hätten Sie das alte Pydantic v1 installiert.
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Separate OpenAPI-Schemas für Eingabe und Ausgabe oder nicht { #separate-openapi-schemas-for-input-and-output-or-not }
|
||||
|
||||
Bei Verwendung von **Pydantic v2** ist die generierte OpenAPI etwas genauer und **korrekter** als zuvor. 😎
|
||||
Seit der Veröffentlichung von **Pydantic v2** ist die generierte OpenAPI etwas genauer und **korrekter** als zuvor. 😎
|
||||
|
||||
Tatsächlich gibt es in einigen Fällen sogar **zwei JSON-Schemas** in OpenAPI für dasselbe Pydantic-Modell, für Eingabe und Ausgabe, je nachdem, ob sie **Defaultwerte** haben.
|
||||
|
||||
@@ -100,5 +100,3 @@ Und jetzt wird es ein einziges Schema für die Eingabe und Ausgabe des Modells g
|
||||
<div class="screenshot">
|
||||
<img src="/img/tutorial/separate-openapi-schemas/image05.png">
|
||||
</div>
|
||||
|
||||
Dies ist das gleiche Verhalten wie in Pydantic v1. 🤓
|
||||
|
||||
@@ -50,14 +50,6 @@ Wenn Sie Teil-Aktualisierungen entgegennehmen, ist der `exclude_unset`-Parameter
|
||||
|
||||
Wie in `item.model_dump(exclude_unset=True)`.
|
||||
|
||||
/// info | Info
|
||||
|
||||
In Pydantic v1 hieß diese Methode `.dict()`, in Pydantic v2 wurde sie <abbr title="veraltet, obsolet: Es soll nicht mehr verwendet werden">deprecatet</abbr> (aber immer noch unterstützt) und in `.model_dump()` umbenannt.
|
||||
|
||||
Die Beispiele hier verwenden `.dict()` für die Kompatibilität mit Pydantic v1, Sie sollten jedoch stattdessen `.model_dump()` verwenden, wenn Sie Pydantic v2 verwenden können.
|
||||
|
||||
///
|
||||
|
||||
Das wird ein <abbr title="Dictionary – Zuordnungstabelle: In anderen Sprachen auch Hash, Map, Objekt, Assoziatives Array genannt">`dict`</abbr> erstellen, mit nur den Daten, die gesetzt wurden, als das `item`-Modell erstellt wurde, Defaultwerte ausgeschlossen.
|
||||
|
||||
Sie können das verwenden, um ein `dict` zu erstellen, das nur die (im <abbr title="Request – Anfrage: Daten, die der Client zum Server sendet">Request</abbr>) gesendeten Daten enthält, ohne Defaultwerte:
|
||||
@@ -68,14 +60,6 @@ Sie können das verwenden, um ein `dict` zu erstellen, das nur die (im <abbr tit
|
||||
|
||||
Jetzt können Sie eine Kopie des existierenden Modells mittels `.model_copy()` erstellen, wobei Sie dem `update`-Parameter ein `dict` mit den zu ändernden Daten übergeben.
|
||||
|
||||
/// info | Info
|
||||
|
||||
In Pydantic v1 hieß diese Methode `.copy()`, in Pydantic v2 wurde sie <abbr title="veraltet, obsolet: Es soll nicht mehr verwendet werden">deprecatet</abbr> (aber immer noch unterstützt) und in `.model_copy()` umbenannt.
|
||||
|
||||
Die Beispiele hier verwenden `.copy()` für die Kompatibilität mit Pydantic v1, Sie sollten jedoch stattdessen `.model_copy()` verwenden, wenn Sie Pydantic v2 verwenden können.
|
||||
|
||||
///
|
||||
|
||||
Wie in `stored_item_model.model_copy(update=update_data)`:
|
||||
|
||||
{* ../../docs_src/body_updates/tutorial002_py310.py hl[33] *}
|
||||
|
||||
@@ -127,14 +127,6 @@ Innerhalb der Funktion können Sie alle Attribute des Modellobjekts direkt verwe
|
||||
|
||||
{* ../../docs_src/body/tutorial002_py310.py *}
|
||||
|
||||
/// info | Info
|
||||
|
||||
In Pydantic v1 hieß die Methode `.dict()`, sie wurde in Pydantic v2 deprecatet (aber weiterhin unterstützt) und in `.model_dump()` umbenannt.
|
||||
|
||||
Die Beispiele hier verwenden `.dict()` zur Kompatibilität mit Pydantic v1, aber Sie sollten stattdessen `.model_dump()` verwenden, wenn Sie Pydantic v2 nutzen können.
|
||||
|
||||
///
|
||||
|
||||
## Requestbody- + Pfad-Parameter { #request-body-path-parameters }
|
||||
|
||||
Sie können Pfad-Parameter und den Requestbody gleichzeitig deklarieren.
|
||||
|
||||
@@ -22,21 +22,13 @@ Hier ist eine allgemeine Idee, wie die Modelle mit ihren Passwortfeldern aussehe
|
||||
|
||||
{* ../../docs_src/extra_models/tutorial001_py310.py hl[7,9,14,20,22,27:28,31:33,38:39] *}
|
||||
|
||||
/// info | Info
|
||||
### Über `**user_in.model_dump()` { #about-user-in-model-dump }
|
||||
|
||||
In Pydantic v1 hieß die Methode `.dict()`, in Pydantic v2 wurde sie <abbr title="veraltet, obsolet: Es soll nicht mehr verwendet werden">deprecatet</abbr> (aber weiterhin unterstützt) und in `.model_dump()` umbenannt.
|
||||
|
||||
Die Beispiele hier verwenden `.dict()` für die Kompatibilität mit Pydantic v1, aber Sie sollten `.model_dump()` verwenden, wenn Sie Pydantic v2 verwenden können.
|
||||
|
||||
///
|
||||
|
||||
### Über `**user_in.dict()` { #about-user-in-dict }
|
||||
|
||||
#### Die `.dict()`-Methode von Pydantic { #pydantics-dict }
|
||||
#### Pydantics `.model_dump()` { #pydantics-model-dump }
|
||||
|
||||
`user_in` ist ein Pydantic-Modell der Klasse `UserIn`.
|
||||
|
||||
Pydantic-Modelle haben eine `.dict()`-Methode, die ein <abbr title="Dictionary – Zuordnungstabelle: In anderen Sprachen auch Hash, Map, Objekt, Assoziatives Array genannt">`dict`</abbr> mit den Daten des Modells zurückgibt.
|
||||
Pydantic-Modelle haben eine `.model_dump()`-Methode, die ein <abbr title="Dictionary – Zuordnungstabelle: In anderen Sprachen auch Hash, Map, Objekt, Assoziatives Array genannt">`dict`</abbr> mit den Daten des Modells zurückgibt.
|
||||
|
||||
Wenn wir also ein Pydantic-Objekt `user_in` erstellen, etwa so:
|
||||
|
||||
@@ -47,7 +39,7 @@ user_in = UserIn(username="john", password="secret", email="john.doe@example.com
|
||||
und dann aufrufen:
|
||||
|
||||
```Python
|
||||
user_dict = user_in.dict()
|
||||
user_dict = user_in.model_dump()
|
||||
```
|
||||
|
||||
haben wir jetzt ein `dict` mit den Daten in der Variablen `user_dict` (es ist ein `dict` statt eines Pydantic-Modellobjekts).
|
||||
@@ -103,20 +95,20 @@ UserInDB(
|
||||
|
||||
#### Ein Pydantic-Modell aus dem Inhalt eines anderen { #a-pydantic-model-from-the-contents-of-another }
|
||||
|
||||
Da wir im obigen Beispiel `user_dict` von `user_in.dict()` bekommen haben, wäre dieser Code:
|
||||
Da wir im obigen Beispiel `user_dict` von `user_in.model_dump()` bekommen haben, wäre dieser Code:
|
||||
|
||||
```Python
|
||||
user_dict = user_in.dict()
|
||||
user_dict = user_in.model_dump()
|
||||
UserInDB(**user_dict)
|
||||
```
|
||||
|
||||
gleichwertig zu:
|
||||
|
||||
```Python
|
||||
UserInDB(**user_in.dict())
|
||||
UserInDB(**user_in.model_dump())
|
||||
```
|
||||
|
||||
... weil `user_in.dict()` ein `dict` ist, und dann lassen wir Python es „entpacken“, indem wir es an `UserInDB` mit vorangestelltem `**` übergeben.
|
||||
... weil `user_in.model_dump()` ein `dict` ist, und dann lassen wir Python es „entpacken“, indem wir es an `UserInDB` mit vorangestelltem `**` übergeben.
|
||||
|
||||
Auf diese Weise erhalten wir ein Pydantic-Modell aus den Daten eines anderen Pydantic-Modells.
|
||||
|
||||
@@ -125,7 +117,7 @@ Auf diese Weise erhalten wir ein Pydantic-Modell aus den Daten eines anderen Pyd
|
||||
Und dann fügen wir das zusätzliche Schlüsselwort-Argument `hashed_password=hashed_password` hinzu, wie in:
|
||||
|
||||
```Python
|
||||
UserInDB(**user_in.dict(), hashed_password=hashed_password)
|
||||
UserInDB(**user_in.model_dump(), hashed_password=hashed_password)
|
||||
```
|
||||
|
||||
... was so ist wie:
|
||||
@@ -180,7 +172,6 @@ Wenn Sie eine <a href="https://docs.pydantic.dev/latest/concepts/types/#unions"
|
||||
|
||||
{* ../../docs_src/extra_models/tutorial003_py310.py hl[1,14:15,18:20,33] *}
|
||||
|
||||
|
||||
### `Union` in Python 3.10 { #union-in-python-3-10 }
|
||||
|
||||
In diesem Beispiel übergeben wir `Union[PlaneItem, CarItem]` als Wert des Arguments `response_model`.
|
||||
@@ -203,7 +194,6 @@ Dafür verwenden Sie Pythons Standard-`typing.List` (oder nur `list` in Python 3
|
||||
|
||||
{* ../../docs_src/extra_models/tutorial004_py39.py hl[18] *}
|
||||
|
||||
|
||||
## Response mit beliebigem `dict` { #response-with-arbitrary-dict }
|
||||
|
||||
Sie können auch eine Response deklarieren, die ein beliebiges `dict` zurückgibt, indem Sie nur die Typen der Schlüssel und Werte ohne ein Pydantic-Modell deklarieren.
|
||||
@@ -214,7 +204,6 @@ In diesem Fall können Sie `typing.Dict` verwenden (oder nur `dict` in Python 3.
|
||||
|
||||
{* ../../docs_src/extra_models/tutorial005_py39.py hl[6] *}
|
||||
|
||||
|
||||
## Zusammenfassung { #recap }
|
||||
|
||||
Verwenden Sie gerne mehrere Pydantic-Modelle und vererben Sie je nach Bedarf.
|
||||
|
||||
@@ -205,20 +205,6 @@ Wenn Sie sich mit all diesen **„regulärer Ausdruck“**-Ideen verloren fühle
|
||||
|
||||
Aber nun wissen Sie, dass Sie sie in **FastAPI** immer dann verwenden können, wenn Sie sie brauchen.
|
||||
|
||||
### Pydantic v1 `regex` statt `pattern` { #pydantic-v1-regex-instead-of-pattern }
|
||||
|
||||
Vor Pydantic Version 2 und FastAPI 0.100.0, hieß der Parameter `regex` statt `pattern`, aber das ist jetzt obsolet.
|
||||
|
||||
Sie könnten immer noch Code sehen, der den alten Namen verwendet:
|
||||
|
||||
//// tab | Pydantic v1
|
||||
|
||||
{* ../../docs_src/query_params_str_validations/tutorial004_regex_an_py310.py hl[11] *}
|
||||
|
||||
////
|
||||
|
||||
Beachten Sie aber, dass das obsolet ist und auf den neuen Parameter `pattern` aktualisiert werden sollte. 🤓
|
||||
|
||||
## Defaultwerte { #default-values }
|
||||
|
||||
Natürlich können Sie Defaultwerte verwenden, die nicht `None` sind.
|
||||
|
||||
@@ -252,20 +252,6 @@ Wenn Sie also den Artikel mit der ID `foo` bei der *Pfadoperation* anfragen, wir
|
||||
|
||||
/// info | Info
|
||||
|
||||
In Pydantic v1 hieß diese Methode `.dict()`, in Pydantic v2 wurde sie <abbr title="veraltet, obsolet: Es soll nicht mehr verwendet werden">deprecatet</abbr> (aber immer noch unterstützt) und in `.model_dump()` umbenannt.
|
||||
|
||||
Die Beispiele hier verwenden `.dict()` für die Kompatibilität mit Pydantic v1, Sie sollten jedoch stattdessen `.model_dump()` verwenden, wenn Sie Pydantic v2 verwenden können.
|
||||
|
||||
///
|
||||
|
||||
/// info | Info
|
||||
|
||||
FastAPI verwendet `.dict()` von Pydantic Modellen, <a href="https://docs.pydantic.dev/1.10/usage/exporting_models/#modeldict" class="external-link" target="_blank">mit dessen `exclude_unset`-Parameter</a>, um das zu erreichen.
|
||||
|
||||
///
|
||||
|
||||
/// info | Info
|
||||
|
||||
Sie können auch:
|
||||
|
||||
* `response_model_exclude_defaults=True`
|
||||
|
||||
@@ -8,36 +8,14 @@ Hier sind mehrere Möglichkeiten, das zu tun.
|
||||
|
||||
Sie können `examples` („Beispiele“) für ein Pydantic-Modell deklarieren, welche dem generierten JSON-Schema hinzugefügt werden.
|
||||
|
||||
//// tab | Pydantic v2
|
||||
|
||||
{* ../../docs_src/schema_extra_example/tutorial001_py310.py hl[13:24] *}
|
||||
|
||||
////
|
||||
|
||||
//// tab | Pydantic v1
|
||||
|
||||
{* ../../docs_src/schema_extra_example/tutorial001_pv1_py310.py hl[13:23] *}
|
||||
|
||||
////
|
||||
|
||||
Diese zusätzlichen Informationen werden unverändert zum für dieses Modell ausgegebenen **JSON-Schema** hinzugefügt und in der API-Dokumentation verwendet.
|
||||
|
||||
//// tab | Pydantic v2
|
||||
|
||||
In Pydantic Version 2 würden Sie das Attribut `model_config` verwenden, das ein <abbr title="Dictionary – Zuordnungstabelle: In anderen Sprachen auch Hash, Map, Objekt, Assoziatives Array genannt">`dict`</abbr> akzeptiert, wie beschrieben in <a href="https://docs.pydantic.dev/latest/api/config/" class="external-link" target="_blank">Pydantic-Dokumentation: Configuration</a>.
|
||||
Sie können das Attribut `model_config` verwenden, das ein <abbr title="Dictionary – Zuordnungstabelle: In anderen Sprachen auch Hash, Map, Objekt, Assoziatives Array genannt">`dict`</abbr> akzeptiert, wie beschrieben in <a href="https://docs.pydantic.dev/latest/api/config/" class="external-link" target="_blank">Pydantic-Dokumentation: Configuration</a>.
|
||||
|
||||
Sie können `json_schema_extra` setzen, mit einem `dict`, das alle zusätzlichen Daten enthält, die im generierten JSON-Schema angezeigt werden sollen, einschließlich `examples`.
|
||||
|
||||
////
|
||||
|
||||
//// tab | Pydantic v1
|
||||
|
||||
In Pydantic Version 1 würden Sie eine interne Klasse `Config` und `schema_extra` verwenden, wie beschrieben in <a href="https://docs.pydantic.dev/1.10/usage/schema/#schema-customization" class="external-link" target="_blank">Pydantic-Dokumentation: Schema customization</a>.
|
||||
|
||||
Sie können `schema_extra` setzen, mit einem `dict`, das alle zusätzlichen Daten enthält, die im generierten JSON-Schema angezeigt werden sollen, einschließlich `examples`.
|
||||
|
||||
////
|
||||
|
||||
/// tip | Tipp
|
||||
|
||||
Mit derselben Technik können Sie das JSON-Schema erweitern und Ihre eigenen benutzerdefinierten Zusatzinformationen hinzufügen.
|
||||
|
||||
BIN
docs/en/docs/img/fastapi-documentary.jpg
Normal file
BIN
docs/en/docs/img/fastapi-documentary.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 187 KiB |
@@ -117,6 +117,12 @@ The key features are:
|
||||
|
||||
---
|
||||
|
||||
## FastAPI mini documentary { #fastapi-mini-documentary }
|
||||
|
||||
There's a <a href="https://www.youtube.com/watch?v=mpR8ngthqiE" class="external-link" target="_blank">FastAPI mini documentary</a> released at the end of 2025, you can watch it online:
|
||||
|
||||
<a href="https://www.youtube.com/watch?v=mpR8ngthqiE" target="_blank"><img src="https://fastapi.tiangolo.com/img/fastapi-documentary.jpg" alt="FastAPI Mini Documentary"></a>
|
||||
|
||||
## **Typer**, the FastAPI of CLIs { #typer-the-fastapi-of-clis }
|
||||
|
||||
<a href="https://typer.tiangolo.com" target="_blank"><img src="https://typer.tiangolo.com/img/logo-margin/logo-margin-vector.svg" style="width: 20%;"></a>
|
||||
|
||||
@@ -7,6 +7,14 @@ hide:
|
||||
|
||||
## Latest Changes
|
||||
|
||||
### Docs
|
||||
|
||||
* 📝 Add documentary to website. PR [#14600](https://github.com/fastapi/fastapi/pull/14600) by [@tiangolo](https://github.com/tiangolo).
|
||||
|
||||
### Translations
|
||||
|
||||
* 🌐 Update translations for de (update-outdated). PR [#14581](https://github.com/fastapi/fastapi/pull/14581) by [@nilslindemann](https://github.com/nilslindemann).
|
||||
|
||||
### Internal
|
||||
|
||||
* 👷 Update secrets check. PR [#14592](https://github.com/fastapi/fastapi/pull/14592) by [@tiangolo](https://github.com/tiangolo).
|
||||
|
||||
694
scripts/cmpr.py
694
scripts/cmpr.py
@@ -1,694 +0,0 @@
|
||||
import os
|
||||
import platform
|
||||
import re
|
||||
import subprocess
|
||||
from collections.abc import Iterable
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import Annotated, Literal, cast
|
||||
|
||||
import typer
|
||||
|
||||
ROOT = Path("../") # assuming this script is in the scripts directory
|
||||
DOCS_ROOT = os.getenv("DOCS_ROOT", "docs")
|
||||
TMP_DOCS_PATH = os.getenv("TMP_DOCS_PATH", "non-git/translations")
|
||||
VSCODE_COMMAND = os.getenv(
|
||||
"VSCODE_COMMAND", "code.cmd" if platform.system() == "Windows" else "code"
|
||||
)
|
||||
|
||||
# TBD: `Literal` is not supported in typer 0.16.0, which is the
|
||||
# version given in the requirements-docs.txt.
|
||||
# Shall we upgrade that requirement to 0.20.0?
|
||||
LANGS = Literal["es", "de", "ru", "pt", "uk", "fr"]
|
||||
|
||||
|
||||
non_translated_sections = (
|
||||
f"reference{os.sep}",
|
||||
"release-notes.md",
|
||||
"fastapi-people.md",
|
||||
"external-links.md",
|
||||
"newsletter.md",
|
||||
"management-tasks.md",
|
||||
"management.md",
|
||||
"contributing.md",
|
||||
)
|
||||
|
||||
|
||||
class Retry(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class CompareError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
@dataclass
|
||||
class Config:
|
||||
lang: LANGS
|
||||
interactive: bool = True
|
||||
check_code_includes: bool = True
|
||||
check_multiline_blocks: bool = True
|
||||
check_headers_and_permalinks: bool = True
|
||||
check_markdown_links: bool = True
|
||||
check_html_links: bool = True
|
||||
full_paths: bool = False
|
||||
|
||||
|
||||
# ===================================================================================
|
||||
# Code includes
|
||||
|
||||
CODE_INCLUDE_RE = re.compile(r"^\{\*\s*(\S+)\s*(.*)\*\}$")
|
||||
|
||||
|
||||
def extract_code_includes(lines: list[str]) -> list[tuple[str, str, str, int]]:
|
||||
includes = []
|
||||
for line_no, line in enumerate(lines, start=1):
|
||||
if CODE_INCLUDE_RE.match(line):
|
||||
includes.append((line_no, line))
|
||||
return includes
|
||||
|
||||
|
||||
def replace_code_includes(source_text: str, target_text: str) -> str:
|
||||
target_lines = target_text.splitlines()
|
||||
source_code_includes = extract_code_includes(source_text.splitlines())
|
||||
target_code_includes = extract_code_includes(target_lines)
|
||||
|
||||
if len(source_code_includes) != len(target_code_includes):
|
||||
raise CompareError(
|
||||
f"Number of code includes differs: "
|
||||
f"{len(source_code_includes)} in source vs {len(target_code_includes)} in target."
|
||||
)
|
||||
|
||||
for src_include, tgt_include in zip(source_code_includes, target_code_includes):
|
||||
_, src_line = src_include
|
||||
tgt_line_no, _ = tgt_include
|
||||
target_lines[tgt_line_no - 1] = src_line
|
||||
|
||||
target_lines.append("") # To preserve the empty line in the end of the file
|
||||
return "\n".join(target_lines)
|
||||
|
||||
|
||||
# ===================================================================================
|
||||
# Multiline code blocks
|
||||
|
||||
LANG_RE = re.compile(r"^```([\w-]*)", re.MULTILINE)
|
||||
|
||||
|
||||
def get_code_block_lang(line: str) -> str:
|
||||
match = LANG_RE.match(line)
|
||||
if match:
|
||||
return match.group(1)
|
||||
return ""
|
||||
|
||||
|
||||
def extract_multiline_blocks(text: str) -> list[tuple[str, int, str]]:
|
||||
lines = text.splitlines()
|
||||
blocks = []
|
||||
|
||||
in_code_block3 = False
|
||||
in_code_block4 = False
|
||||
current_block_lang = ""
|
||||
current_block_start_line = -1
|
||||
current_block_lines = []
|
||||
|
||||
for line_no, line in enumerate(lines, start=1):
|
||||
stripped = line.lstrip()
|
||||
|
||||
# --- Detect opening fence ---
|
||||
if not (in_code_block3 or in_code_block4):
|
||||
if stripped.startswith("```"):
|
||||
current_block_start_line = line_no
|
||||
count = len(stripped) - len(stripped.lstrip("`"))
|
||||
if count == 3:
|
||||
in_code_block3 = True
|
||||
current_block_lang = get_code_block_lang(stripped)
|
||||
current_block_lines = [line]
|
||||
continue
|
||||
elif count >= 4:
|
||||
in_code_block4 = True
|
||||
current_block_lang = get_code_block_lang(stripped)
|
||||
current_block_lines = [line]
|
||||
continue
|
||||
|
||||
# --- Detect closing fence ---
|
||||
elif in_code_block3:
|
||||
if stripped.startswith("```"):
|
||||
count = len(stripped) - len(stripped.lstrip("`"))
|
||||
if count == 3:
|
||||
current_block_lines.append(line)
|
||||
blocks.append(
|
||||
(
|
||||
current_block_lang,
|
||||
current_block_start_line,
|
||||
"\n".join(current_block_lines),
|
||||
)
|
||||
)
|
||||
in_code_block3 = False
|
||||
current_block_lang = ""
|
||||
current_block_start_line = -1
|
||||
continue
|
||||
current_block_lines.append(line)
|
||||
|
||||
elif in_code_block4:
|
||||
if stripped.startswith("````"):
|
||||
count = len(stripped) - len(stripped.lstrip("`"))
|
||||
if count >= 4:
|
||||
current_block_lines.append(line)
|
||||
blocks.append(
|
||||
(
|
||||
current_block_lang,
|
||||
current_block_start_line,
|
||||
"\n".join(current_block_lines),
|
||||
)
|
||||
)
|
||||
in_code_block4 = False
|
||||
current_block_lang = ""
|
||||
current_block_start_line = -1
|
||||
continue
|
||||
current_block_lines.append(line)
|
||||
|
||||
return blocks
|
||||
|
||||
|
||||
def replace_blocks(source_text: str, target_text: str) -> str:
|
||||
source_blocks = extract_multiline_blocks(source_text)
|
||||
target_blocks = extract_multiline_blocks(target_text)
|
||||
|
||||
if len(source_blocks) != len(target_blocks):
|
||||
raise CompareError(
|
||||
f"Number of code blocks differs: "
|
||||
f"{len(source_blocks)} in source vs {len(target_blocks)} in target."
|
||||
)
|
||||
|
||||
for i, ((src_lang, *_), (tgt_lang, tgt_line_no, *_)) in enumerate(
|
||||
zip(source_blocks, target_blocks), 1
|
||||
):
|
||||
if src_lang != tgt_lang:
|
||||
raise CompareError(
|
||||
f"Type mismatch in block #{i} (line {tgt_line_no}): "
|
||||
f"'{src_lang or '(no lang)'}' vs '{tgt_lang or '(no lang)'}'"
|
||||
)
|
||||
|
||||
# Sequentially replace each block in target with the one from source
|
||||
result = target_text
|
||||
for (*_, src_block), (*_, tgt_block) in zip(source_blocks, target_blocks):
|
||||
result = result.replace(tgt_block, src_block, 1)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
# ===================================================================================
|
||||
# Headers and permalinks
|
||||
|
||||
header_with_permalink_pattern = re.compile(r"^(#{1,6}) (.+?)(\s*\{\s*#.*\s*\})?\s*$")
|
||||
|
||||
|
||||
def extract_headers_and_permalinks(lines: list[str]) -> list[tuple[str, int, str]]:
|
||||
headers = []
|
||||
in_code_block3 = False
|
||||
in_code_block4 = False
|
||||
|
||||
for line_no, line in enumerate(lines, start=1):
|
||||
if not (in_code_block3 or in_code_block4):
|
||||
if line.startswith("```"):
|
||||
count = len(line) - len(line.lstrip("`"))
|
||||
if count == 3:
|
||||
in_code_block3 = True
|
||||
continue
|
||||
elif count >= 4:
|
||||
in_code_block4 = True
|
||||
continue
|
||||
|
||||
header_match = header_with_permalink_pattern.match(line)
|
||||
if header_match:
|
||||
hashes, _title, permalink = header_match.groups()
|
||||
headers.append((hashes, line_no, permalink))
|
||||
|
||||
elif in_code_block3:
|
||||
if line.startswith("```"):
|
||||
count = len(line) - len(line.lstrip("`"))
|
||||
if count == 3:
|
||||
in_code_block3 = False
|
||||
continue
|
||||
|
||||
elif in_code_block4:
|
||||
if line.startswith("````"):
|
||||
count = len(line) - len(line.lstrip("`"))
|
||||
if count >= 4:
|
||||
in_code_block4 = False
|
||||
continue
|
||||
|
||||
return headers
|
||||
|
||||
|
||||
def replace_headers_and_permalinks(source_text: str, target_text: str) -> str:
|
||||
target_lines = target_text.splitlines()
|
||||
|
||||
source_headers = extract_headers_and_permalinks(source_text.splitlines())
|
||||
target_headers = extract_headers_and_permalinks(target_lines)
|
||||
|
||||
if len(source_headers) != len(target_headers):
|
||||
raise CompareError(
|
||||
f"Number of headers differs: "
|
||||
f"{len(source_headers)} in source vs {len(target_headers)} in target."
|
||||
)
|
||||
|
||||
for i, ((src_hashes, *_), (tgt_hashes, tgt_line_no, *_)) in enumerate(
|
||||
zip(source_headers, target_headers), 1
|
||||
):
|
||||
if src_hashes != tgt_hashes:
|
||||
raise CompareError(
|
||||
f"Header level mismatch in #{i} (line {tgt_line_no}): "
|
||||
"'{src_hashes}' vs '{tgt_hashes}'"
|
||||
)
|
||||
|
||||
# Sequentially replace each header permalink in target with the one from source
|
||||
for src_header, tgt_header in zip(source_headers, target_headers):
|
||||
src_permalink = src_header[2]
|
||||
tgt_line_no = tgt_header[1] - 1 # Convert from 1-based to 0-based
|
||||
header_match = header_with_permalink_pattern.match(target_lines[tgt_line_no])
|
||||
if header_match:
|
||||
hashes, title, _ = header_match.groups()
|
||||
target_lines[tgt_line_no] = (
|
||||
f"{hashes} {title}{src_permalink or ' (ERROR - MISSING PERMALINK)'}"
|
||||
)
|
||||
|
||||
target_lines.append("") # To preserve the empty line in the end of the file
|
||||
return "\n".join(target_lines)
|
||||
|
||||
|
||||
# ===================================================================================
|
||||
# Links
|
||||
|
||||
MARKDOWN_LINK_RE = re.compile(
|
||||
r"(?<!\!)" # not an image ![...]
|
||||
r"\[(?P<text>.*?)\]" # link text (non-greedy)
|
||||
r"\("
|
||||
r"(?P<url>\S+?)" # url (no spaces, non-greedy)
|
||||
r'(?:\s+["\'](?P<title>.*?)["\'])?' # optional title in "" or ''
|
||||
r"\)"
|
||||
)
|
||||
|
||||
|
||||
def extract_markdown_links(lines: list[str]) -> list[tuple[str, int]]:
|
||||
links = []
|
||||
for line_no, line in enumerate(lines, start=1):
|
||||
for m in MARKDOWN_LINK_RE.finditer(line):
|
||||
url = m.group("url")
|
||||
links.append((url, line_no))
|
||||
return links
|
||||
|
||||
|
||||
def replace_markdown_links(source_text: str, target_text: str, lang: str) -> str:
|
||||
target_lines = target_text.splitlines()
|
||||
source_links = extract_markdown_links(source_text.splitlines())
|
||||
target_links = extract_markdown_links(target_lines)
|
||||
|
||||
if len(source_links) != len(target_links):
|
||||
raise CompareError(
|
||||
f"Number of markdown links differs: "
|
||||
f"{len(source_links)} in source vs {len(target_links)} in target."
|
||||
)
|
||||
|
||||
# Sequentially replace each link URL in target with the one from source
|
||||
for (src_link, _), (tgt_link, tgt_line_no) in zip(source_links, target_links):
|
||||
real_line_no = tgt_line_no - 1 # Convert to zero-based
|
||||
line = target_lines[real_line_no]
|
||||
link_replace = add_lang_code_if_needed(src_link, tgt_link, lang)
|
||||
target_lines[real_line_no] = line.replace(tgt_link, link_replace)
|
||||
|
||||
target_lines.append("") # To preserve the empty line in the end of the file
|
||||
return "\n".join(target_lines)
|
||||
|
||||
|
||||
HTML_LINK_RE = re.compile(r"<a\s+[^>]*>.*?</a>")
|
||||
HTML_LINK_TEXT = re.compile(r"<a\b([^>]*)>(.*?)</a>")
|
||||
HTML_LINK_OPEN_TAG_RE = re.compile(r"<a\b([^>]*)>")
|
||||
HTML_ATTR_RE = re.compile(r'(\w+)\s*=\s*([\'"])(.*?)\2')
|
||||
|
||||
|
||||
def extract_html_links(
|
||||
lines: list[str],
|
||||
) -> list[tuple[tuple[str, list[tuple[str, str, str]], str], int]]:
|
||||
links = []
|
||||
for line_no, line in enumerate(lines, start=1):
|
||||
for html_link in HTML_LINK_RE.finditer(line):
|
||||
link_str = html_link.group(0)
|
||||
link_text = cast(re.Match, HTML_LINK_TEXT.match(link_str)).group(2)
|
||||
link_data = (link_str, [], link_text)
|
||||
link_open_tag = cast(re.Match, HTML_LINK_OPEN_TAG_RE.match(link_str)).group(
|
||||
1
|
||||
)
|
||||
attributes = re.findall(HTML_ATTR_RE, link_open_tag)
|
||||
for attr_data in attributes:
|
||||
link_data[1].append(attr_data)
|
||||
links.append((link_data, line_no))
|
||||
return links
|
||||
|
||||
|
||||
TIANGOLO_COM = "https://fastapi.tiangolo.com"
|
||||
|
||||
|
||||
def add_lang_code_if_needed(url: str, prev_url: str, lang_code: str) -> str:
|
||||
if url.startswith(TIANGOLO_COM):
|
||||
if prev_url.startswith(f"{TIANGOLO_COM}/{lang_code}"):
|
||||
url = url.replace(TIANGOLO_COM, f"{TIANGOLO_COM}/{lang_code}")
|
||||
return url
|
||||
|
||||
|
||||
def reconstruct_html_link(
|
||||
attributes: list[tuple[str, str, str]],
|
||||
link_text: str,
|
||||
prev_attributes: list[tuple[str, str, str]],
|
||||
lang_code: str,
|
||||
) -> str:
|
||||
prev_attributes_dict = {attr[0]: attr[2] for attr in prev_attributes}
|
||||
prev_url = prev_attributes_dict["href"]
|
||||
attributes_upd = []
|
||||
for attr_name, attr_quotes, attr_value in attributes:
|
||||
if attr_name == "href":
|
||||
attr_value = add_lang_code_if_needed(attr_value, prev_url, lang_code)
|
||||
attributes_upd.append((attr_name, attr_quotes, attr_value))
|
||||
|
||||
attrs_str = " ".join(
|
||||
f"{name}={quetes}{value}{quetes}" for name, quetes, value in attributes_upd
|
||||
)
|
||||
return f"<a {attrs_str}>{link_text}</a>"
|
||||
|
||||
|
||||
def replace_html_links(source_text: str, target_text: str, lang: str) -> str:
|
||||
target_lines = target_text.splitlines()
|
||||
source_links = extract_html_links(source_text.splitlines())
|
||||
target_links = extract_html_links(target_lines)
|
||||
|
||||
if len(source_links) != len(target_links):
|
||||
raise CompareError(
|
||||
f"Number of HTML links differs: "
|
||||
f"{len(source_links)} in source vs {len(target_links)} in target."
|
||||
)
|
||||
|
||||
# Sequentially replace attributes of each link URL in target with the one from source
|
||||
for (src_link_data, _), (tgt_link_data, tgt_line_no) in zip(
|
||||
source_links, target_links
|
||||
):
|
||||
real_line_no = tgt_line_no - 1 # Convert to zero-based
|
||||
line = target_lines[real_line_no]
|
||||
tgt_link_text = tgt_link_data[2]
|
||||
|
||||
tgt_link_original = tgt_link_data[0]
|
||||
tgt_link_override = reconstruct_html_link(
|
||||
src_link_data[1], tgt_link_text, tgt_link_data[1], lang
|
||||
)
|
||||
target_lines[real_line_no] = line.replace(tgt_link_original, tgt_link_override)
|
||||
|
||||
target_lines.append("") # To preserve the empty line in the end of the file
|
||||
return "\n".join(target_lines)
|
||||
|
||||
|
||||
# ===================================================================================
|
||||
# Images
|
||||
|
||||
|
||||
# ===================================================================================
|
||||
# Helper functions
|
||||
|
||||
|
||||
def get_lang_doc_root_dir(lang: str) -> Path:
|
||||
return ROOT / DOCS_ROOT / lang / "docs"
|
||||
|
||||
|
||||
def iter_all_lang_paths(lang_path_root: Path) -> Iterable[Path]:
|
||||
"""
|
||||
Iterate on the markdown files to translate in order of priority.
|
||||
"""
|
||||
|
||||
first_dirs = [
|
||||
lang_path_root / "learn",
|
||||
lang_path_root / "tutorial",
|
||||
lang_path_root / "advanced",
|
||||
lang_path_root / "about",
|
||||
lang_path_root / "how-to",
|
||||
]
|
||||
first_parent = lang_path_root
|
||||
yield from first_parent.glob("*.md")
|
||||
for dir_path in first_dirs:
|
||||
yield from dir_path.rglob("*.md")
|
||||
first_dirs_str = tuple(str(d) for d in first_dirs)
|
||||
for path in lang_path_root.rglob("*.md"):
|
||||
if str(path).startswith(first_dirs_str):
|
||||
continue
|
||||
if path.parent == first_parent:
|
||||
continue
|
||||
yield path
|
||||
|
||||
|
||||
def get_all_paths(lang: str):
|
||||
res: list[str] = []
|
||||
lang_docs_root = get_lang_doc_root_dir(lang)
|
||||
for path in iter_all_lang_paths(lang_docs_root):
|
||||
relpath = path.relative_to(lang_docs_root)
|
||||
if not str(relpath).startswith(non_translated_sections):
|
||||
res.append(str(relpath))
|
||||
return res
|
||||
|
||||
|
||||
# ===================================================================================
|
||||
# Main
|
||||
|
||||
|
||||
def process_one_file_with_retry(document_path: str, config: Config) -> bool:
|
||||
en_docs_root_path = Path(get_lang_doc_root_dir("en"))
|
||||
lang_docs_root_path = Path(get_lang_doc_root_dir(config.lang))
|
||||
while True:
|
||||
try:
|
||||
return process_one_file(
|
||||
en_docs_root_path / document_path,
|
||||
lang_docs_root_path / document_path,
|
||||
config=config,
|
||||
)
|
||||
except Retry: # Retry is only raised in interactive mode
|
||||
pass
|
||||
|
||||
|
||||
def process_one_file(
|
||||
en_doc_path_str: Path, lang_doc_path_str: Path, config: Config
|
||||
) -> bool:
|
||||
en_doc_path = Path(en_doc_path_str)
|
||||
lang_doc_path = Path(lang_doc_path_str)
|
||||
if not en_doc_path.exists():
|
||||
print(
|
||||
f"{'❌🔎 ' if config.interactive else ''}{en_doc_path_str} - doesn't exist"
|
||||
)
|
||||
return False
|
||||
|
||||
en_doc_text = en_doc_path.read_text(encoding="utf-8")
|
||||
lang_doc_text = lang_doc_path.read_text(encoding="utf-8")
|
||||
lang_doc_text_orig = lang_doc_text
|
||||
|
||||
try:
|
||||
if config.check_code_includes:
|
||||
lang_doc_text = replace_code_includes(
|
||||
source_text=en_doc_text,
|
||||
target_text=lang_doc_text,
|
||||
)
|
||||
if config.check_multiline_blocks:
|
||||
lang_doc_text = replace_blocks(
|
||||
source_text=en_doc_text,
|
||||
target_text=lang_doc_text,
|
||||
)
|
||||
if config.check_headers_and_permalinks:
|
||||
lang_doc_text = replace_headers_and_permalinks(
|
||||
source_text=en_doc_text,
|
||||
target_text=lang_doc_text,
|
||||
)
|
||||
if config.check_markdown_links:
|
||||
lang_doc_text = replace_markdown_links(
|
||||
source_text=en_doc_text,
|
||||
target_text=lang_doc_text,
|
||||
lang=config.lang,
|
||||
)
|
||||
if config.check_html_links:
|
||||
lang_doc_text = replace_html_links(
|
||||
source_text=en_doc_text,
|
||||
target_text=lang_doc_text,
|
||||
lang=config.lang,
|
||||
)
|
||||
|
||||
except CompareError as e:
|
||||
print(f"{'❔❌ ' if config.interactive else ''}{lang_doc_path_str} Error: {e}")
|
||||
if not config.interactive:
|
||||
return False
|
||||
subprocess.run([VSCODE_COMMAND, "--diff", lang_doc_path_str, en_doc_path_str])
|
||||
resp = ""
|
||||
while resp not in ("f", "e"):
|
||||
resp = input(
|
||||
" Check the diff, fix the problem, and then type F if it's fixed or E to mark as invalid and skip: "
|
||||
)
|
||||
if resp.lower() == "e":
|
||||
print(f"❌ {lang_doc_path_str} skipped with error")
|
||||
return
|
||||
print(f"Check {lang_doc_path_str} again")
|
||||
raise Retry() from None
|
||||
|
||||
if lang_doc_text_orig != lang_doc_text:
|
||||
print(
|
||||
f"{'❔🆚 ' if config.interactive else ''}{lang_doc_path_str} - non-empty diff"
|
||||
)
|
||||
if not config.interactive:
|
||||
return False
|
||||
tmp_path = ROOT / TMP_DOCS_PATH / Path(lang_doc_path_str)
|
||||
tmp_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
tmp_path.write_text(lang_doc_text, encoding="utf-8")
|
||||
subprocess.run(
|
||||
[VSCODE_COMMAND, "--diff", str(lang_doc_path_str), str(tmp_path)]
|
||||
)
|
||||
resp = ""
|
||||
while resp not in ("f", "e"):
|
||||
resp = input(
|
||||
" Check the diff, fix the problem, and then type F to mark it as fixed or E to to mark as invalid and skip: "
|
||||
).lower()
|
||||
if resp == "e":
|
||||
print(f"❌ {lang_doc_path_str} skipped with non-empty diff")
|
||||
return
|
||||
|
||||
print(f"{'✅ ' if config.interactive else ''}{lang_doc_path_str} - Ok")
|
||||
return True
|
||||
|
||||
|
||||
# ===================================================================================
|
||||
# Typer app
|
||||
|
||||
cli = typer.Typer()
|
||||
|
||||
|
||||
@cli.callback()
|
||||
def callback():
|
||||
pass
|
||||
|
||||
|
||||
@cli.callback()
|
||||
def main(
|
||||
ctx: typer.Context,
|
||||
lang: Annotated[LANGS, typer.Option()],
|
||||
interactive: Annotated[
|
||||
bool,
|
||||
typer.Option(
|
||||
help="If True, will open VSCode diffs for each change to fix and confirm.",
|
||||
),
|
||||
] = True,
|
||||
full_paths: Annotated[
|
||||
bool,
|
||||
typer.Option(
|
||||
help="If True, the provided document paths are treated as full paths.",
|
||||
),
|
||||
] = False,
|
||||
check_code_includes: Annotated[
|
||||
bool,
|
||||
typer.Option(
|
||||
help="If True, will compare code includes blocks.",
|
||||
),
|
||||
] = True,
|
||||
check_multiline_blocks: Annotated[
|
||||
bool,
|
||||
typer.Option(
|
||||
help="If True, will compare multiline code blocks.",
|
||||
),
|
||||
] = True,
|
||||
check_headers_and_permalinks: Annotated[
|
||||
bool,
|
||||
typer.Option(
|
||||
help="If True, will compare headers and permalinks.",
|
||||
),
|
||||
] = True,
|
||||
check_markdown_links: Annotated[
|
||||
bool,
|
||||
typer.Option(
|
||||
help="If True, will compare markdown links.",
|
||||
),
|
||||
] = True,
|
||||
check_html_links: Annotated[
|
||||
bool,
|
||||
typer.Option(
|
||||
help="If True, will compare HTML links.",
|
||||
),
|
||||
] = True,
|
||||
):
|
||||
ctx.obj = Config(
|
||||
lang=lang,
|
||||
interactive=interactive,
|
||||
full_paths=full_paths,
|
||||
check_code_includes=check_code_includes,
|
||||
check_multiline_blocks=check_multiline_blocks,
|
||||
check_headers_and_permalinks=check_headers_and_permalinks,
|
||||
check_markdown_links=check_markdown_links,
|
||||
check_html_links=check_html_links,
|
||||
)
|
||||
|
||||
|
||||
@cli.command()
|
||||
def process_all(
|
||||
ctx: typer.Context,
|
||||
):
|
||||
"""
|
||||
Go through all documents of language and compare special blocks with the corresponding
|
||||
blocks in English versions of those documents.
|
||||
"""
|
||||
config = cast(Config, ctx.obj)
|
||||
lang_docs_root_path = get_lang_doc_root_dir(config.lang)
|
||||
docs = get_all_paths(config.lang)
|
||||
|
||||
all_good = True
|
||||
pages_with_errors: list[str] = []
|
||||
for doc in docs:
|
||||
res = process_one_file_with_retry(document_path=doc, config=config)
|
||||
all_good = all_good and res
|
||||
if not res:
|
||||
pages_with_errors.append(doc)
|
||||
|
||||
if not all_good:
|
||||
print("Some documents had errors:")
|
||||
docs_path = lang_docs_root_path.relative_to(ROOT)
|
||||
for page in pages_with_errors:
|
||||
print(f" - {docs_path / page}")
|
||||
raise typer.Exit(code=1)
|
||||
|
||||
|
||||
@cli.command()
|
||||
def process_pages(
|
||||
doc_paths: Annotated[
|
||||
list[str],
|
||||
typer.Argument(
|
||||
help="List of relative paths to the EN documents. Should be relative to docs/en/docs/",
|
||||
),
|
||||
],
|
||||
ctx: typer.Context,
|
||||
):
|
||||
"""
|
||||
Compare special blocks of specified EN documents with the corresponding blocks in
|
||||
translated versions of those documents.
|
||||
"""
|
||||
|
||||
config = cast(Config, ctx.obj)
|
||||
lang_docs_root_path = get_lang_doc_root_dir(config.lang)
|
||||
|
||||
all_good = True
|
||||
pages_with_errors: list[str] = []
|
||||
for doc_path in doc_paths:
|
||||
if config.full_paths:
|
||||
path = ROOT / doc_path.lstrip("/")
|
||||
doc_path = str(path.relative_to(lang_docs_root_path))
|
||||
res = process_one_file_with_retry(document_path=doc_path, config=config)
|
||||
all_good = all_good and res
|
||||
if not res:
|
||||
pages_with_errors.append(doc_path)
|
||||
|
||||
if not all_good:
|
||||
print("Some documents had errors:")
|
||||
docs_path = lang_docs_root_path.relative_to(ROOT)
|
||||
for page in pages_with_errors:
|
||||
print(f" - {docs_path / page}")
|
||||
raise typer.Exit(code=1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
cli()
|
||||
Reference in New Issue
Block a user