From dd7f94d9a3ab9eeb8c4a61f368187ade2e66889c Mon Sep 17 00:00:00 2001 From: bnbong Date: Fri, 19 Jun 2026 14:08:43 +0900 Subject: [PATCH] [DOCS] zh translation --- docs/de/reference/translation-status.md | 4 +- docs/en/reference/translation-status.md | 4 +- docs/es/reference/translation-status.md | 4 +- docs/fr/reference/translation-status.md | 4 +- docs/ja/reference/translation-status.md | 4 +- docs/ko/reference/translation-status.md | 4 +- docs/zh/changelog.md | 1 + docs/zh/contributing/code-guidelines.md | 748 +++++++ docs/zh/contributing/development-setup.md | 816 ++++++++ .../contributing/template-creation-guide.md | 575 ++++++ docs/zh/contributing/translation-guide.md | 367 ++++ docs/zh/index.md | 577 ++++++ docs/zh/reference/faq.md | 784 ++++++++ docs/zh/reference/preset-feature-matrix.md | 60 + .../reference/template-quality-assurance.md | 219 +++ docs/zh/reference/translation-status.md | 82 + docs/zh/tutorial/async-crud-api.md | 665 +++++++ docs/zh/tutorial/basic-api-server.md | 398 ++++ docs/zh/tutorial/custom-response-handling.md | 1393 +++++++++++++ docs/zh/tutorial/database-integration.md | 1027 ++++++++++ docs/zh/tutorial/docker-deployment.md | 1177 +++++++++++ docs/zh/tutorial/domain-starter.md | 392 ++++ docs/zh/tutorial/first-project.md | 1252 ++++++++++++ docs/zh/tutorial/getting-started.md | 564 ++++++ docs/zh/tutorial/mcp-integration.md | 1730 +++++++++++++++++ docs/zh/user-guide/adding-routes.md | 581 ++++++ docs/zh/user-guide/choosing-a-starter.md | 145 ++ docs/zh/user-guide/cli-reference.md | 832 ++++++++ docs/zh/user-guide/creating-projects.md | 540 +++++ docs/zh/user-guide/installation.md | 209 ++ docs/zh/user-guide/quick-start.md | 368 ++++ docs/zh/user-guide/using-templates.md | 608 ++++++ 32 files changed, 16122 insertions(+), 12 deletions(-) create mode 100644 docs/zh/changelog.md create mode 100644 docs/zh/contributing/code-guidelines.md create mode 100644 docs/zh/contributing/development-setup.md create mode 100644 docs/zh/contributing/template-creation-guide.md create mode 100644 docs/zh/contributing/translation-guide.md create mode 100644 docs/zh/index.md create mode 100644 docs/zh/reference/faq.md create mode 100644 docs/zh/reference/preset-feature-matrix.md create mode 100644 docs/zh/reference/template-quality-assurance.md create mode 100644 docs/zh/reference/translation-status.md create mode 100644 docs/zh/tutorial/async-crud-api.md create mode 100644 docs/zh/tutorial/basic-api-server.md create mode 100644 docs/zh/tutorial/custom-response-handling.md create mode 100644 docs/zh/tutorial/database-integration.md create mode 100644 docs/zh/tutorial/docker-deployment.md create mode 100644 docs/zh/tutorial/domain-starter.md create mode 100644 docs/zh/tutorial/first-project.md create mode 100644 docs/zh/tutorial/getting-started.md create mode 100644 docs/zh/tutorial/mcp-integration.md create mode 100644 docs/zh/user-guide/adding-routes.md create mode 100644 docs/zh/user-guide/choosing-a-starter.md create mode 100644 docs/zh/user-guide/cli-reference.md create mode 100644 docs/zh/user-guide/creating-projects.md create mode 100644 docs/zh/user-guide/installation.md create mode 100644 docs/zh/user-guide/quick-start.md create mode 100644 docs/zh/user-guide/using-templates.md diff --git a/docs/de/reference/translation-status.md b/docs/de/reference/translation-status.md index f63f9ee..0473b62 100644 --- a/docs/de/reference/translation-status.md +++ b/docs/de/reference/translation-status.md @@ -21,12 +21,12 @@ Die unten stehenden Zahlen zählen die Markdown-Seiten im Verzeichnisbaum jeder | 🇬🇧 Englisch (`en`) | ✅ Maßgebliche Referenz | 26 / 26 | Maßgeblich. | | 🇰🇷 Koreanisch (`ko`) | ✅ Vollständig | 26 / 26 | Alle Seiten dieser Sprachversion sind vorhanden. Phase 1: oberste Ebene + Kern des Benutzerhandbuchs; Phase 2: restliches Benutzerhandbuch + alle Tutorials; Phase 3: Beiträge + Referenzseiten. `docs/ko/changelog.md` verweist bewusst auf das kanonische englische `CHANGELOG.md`. | | 🇯🇵 Japanisch (`ja`) | ✅ Vollständig | 26 / 26 | Alle Seiten dieser Sprachversion sind vorhanden. Phase 1: oberste Ebene + Kern des Benutzerhandbuchs; Phase 2: restliches Benutzerhandbuch + alle Tutorials; Phase 3: Beiträge + Referenzseiten. `docs/ja/changelog.md` verweist bewusst auf das kanonische englische `CHANGELOG.md`. | -| 🇨🇳 Chinesisch (`zh`) | 🔴 Skelett | 0 / 26 | Nur als Sprach-Build-Ziel vorhanden. Jede Seite fällt auf Englisch zurück. | +| 🇨🇳 Chinesisch (`zh`) | ✅ Vollständig | 26 / 26 | Alle Seiten dieser Sprachversion sind vorhanden. Phase 1: oberste Ebene + Kern des Benutzerhandbuchs; Phase 2: restliches Benutzerhandbuch + alle Tutorials; Phase 3: Beiträge + Referenzseiten. `docs/zh/changelog.md` verweist bewusst auf das kanonische englische `CHANGELOG.md`. | | 🇪🇸 Spanisch (`es`) | ✅ Vollständig | 26 / 26 | Alle Seiten dieser Sprachversion sind vorhanden. Phase 1: oberste Ebene + Kern des Benutzerhandbuchs; Phase 2: restliches Benutzerhandbuch + alle Tutorials; Phase 3: Beiträge + Referenzseiten. `docs/es/changelog.md` verweist bewusst auf das kanonische englische `CHANGELOG.md`. | | 🇫🇷 Französisch (`fr`) | ✅ Vollständig | 26 / 26 | Alle Seiten dieser Sprachversion sind vorhanden. Phase 1: oberste Ebene + Kern des Benutzerhandbuchs; Phase 2: restliches Benutzerhandbuch + alle Tutorials; Phase 3: Beiträge + Referenzseiten. `docs/fr/changelog.md` verweist bewusst auf das kanonische englische `CHANGELOG.md`. | | 🇩🇪 Deutsch (`de`) | ✅ Vollständig | 26 / 26 | Alle Seiten dieser Sprachversion sind vorhanden. Phase 1: oberste Ebene + Kern des Benutzerhandbuchs; Phase 2: restliches Benutzerhandbuch + alle Tutorials; Phase 3: Beiträge + Referenzseiten. `docs/de/changelog.md` verweist bewusst auf das kanonische englische `CHANGELOG.md`. | -*Stand verifiziert am 2026-05-17; die Zeile `de` wurde für den aktuellen Branch nach dem Abschluss von Phase 3 (Beiträge + Referenzseiten) neu gezählt. Deutsch umfasst nun alle Seiten dieser Sprachversion, und `docs/de/changelog.md` verweist auf den kanonischen englischen Changelog.* Diese Zahlen werden manuell gepflegt; um den aktuellen Stand vom Repo-Root neu zu zählen, führen Sie aus: +*Stand verifiziert am 2026-06-18; die Zeile `zh` wurde für den aktuellen Branch nach dem Abschluss von Phase 3 (Beiträge + Referenzseiten) neu gezählt. Chinesisch hat nun alle 26 Locale-Seiten und wird als ✅ Vollständig geführt.* Diese Zahlen werden manuell gepflegt; um den aktuellen Stand vom Repo-Root neu zu zählen, führen Sie aus: ```console $ for loc in en ko ja zh es fr de; do diff --git a/docs/en/reference/translation-status.md b/docs/en/reference/translation-status.md index 2627235..49d4861 100644 --- a/docs/en/reference/translation-status.md +++ b/docs/en/reference/translation-status.md @@ -36,12 +36,12 @@ next section explains). | 🇬🇧 English (`en`) | ✅ Source of truth | 26 / 26 | Authoritative. | | 🇰🇷 Korean (`ko`) | ✅ Complete | 26 / 26 | All locale pages are present. Phase 1: top-level + core user-guide; Phase 2: remaining user-guide + all tutorials; Phase 3: contributing + reference. `docs/ko/changelog.md` intentionally reuses the canonical English `CHANGELOG.md`. | | 🇯🇵 Japanese (`ja`) | ✅ Complete | 26 / 26 | All locale pages are present. Phase 1: top-level + core user-guide; Phase 2: remaining user-guide + all tutorials; Phase 3: contributing + reference. `docs/ja/changelog.md` intentionally reuses the canonical English `CHANGELOG.md`. | -| 🇨🇳 Chinese (`zh`) | 🔴 Skeleton | 0 / 26 | Build target only. Every page falls back to English. | +| 🇨🇳 Chinese (`zh`) | ✅ Complete | 26 / 26 | All locale pages are present. Phase 1: top-level + core user-guide; Phase 2: remaining user-guide + all tutorials; Phase 3: contributing + reference. `docs/zh/changelog.md` intentionally reuses the canonical English `CHANGELOG.md`. | | 🇪🇸 Spanish (`es`) | ✅ Complete | 26 / 26 | All locale pages are present. Phase 1: top-level + core user-guide; Phase 2: remaining user-guide + all tutorials; Phase 3: contributing + reference. `docs/es/changelog.md` intentionally reuses the canonical English `CHANGELOG.md`. | | 🇫🇷 French (`fr`) | ✅ Complete | 26 / 26 | All locale pages are present. Phase 1: top-level + core user-guide; Phase 2: remaining user-guide + all tutorials; Phase 3: contributing + reference. `docs/fr/changelog.md` intentionally reuses the canonical English `CHANGELOG.md`. | | 🇩🇪 German (`de`) | ✅ Complete | 26 / 26 | All locale pages are present. Phase 1: top-level + core user-guide; Phase 2: remaining user-guide + all tutorials; Phase 3: contributing + reference. `docs/de/changelog.md` intentionally reuses the canonical English `CHANGELOG.md`. | -*Snapshot verified 2026-05-17; de row recounted for the current branch after Phase 3 (contributing + reference) landed. German now has all locale pages present, while `docs/de/changelog.md` intentionally points to the canonical English changelog.* These counts are maintained by hand; +*Snapshot verified 2026-06-18; zh row recounted for the current branch after Phase 3 (contributing + reference) landed. Chinese now has all 26 locale pages present and is tracked at ✅ Complete.* These counts are maintained by hand; to recount the current state from the repo root, run: ```console diff --git a/docs/es/reference/translation-status.md b/docs/es/reference/translation-status.md index c456dd2..3c897d5 100644 --- a/docs/es/reference/translation-status.md +++ b/docs/es/reference/translation-status.md @@ -22,11 +22,11 @@ Los números siguientes cuentan páginas Markdown presentes en el árbol de cada | 🇰🇷 Coreano (`ko`) | ✅ Completo | 26 / 26 | Todas las páginas del idioma están presentes. Phase 1: nivel superior + user-guide principal; Phase 2: user-guide restante + todos los tutorials; Phase 3: contributing + reference. `docs/ko/changelog.md` reutiliza intencionadamente el `CHANGELOG.md` canónico en inglés. | | 🇯🇵 Japonés (`ja`) | ✅ Completo | 26 / 26 | Todas las páginas del idioma están presentes. Phase 1: nivel superior + user-guide principal; Phase 2: user-guide restante + todos los tutorials; Phase 3: contributing + reference. `docs/ja/changelog.md` reutiliza intencionadamente el `CHANGELOG.md` canónico en inglés. | | 🇪🇸 Español (`es`) | ✅ Completo | 26 / 26 | Todas las páginas del idioma están presentes. Phase 1: nivel superior + user-guide principal; Phase 2: user-guide restante + todos los tutorials; Phase 3: contributing + reference. `docs/es/changelog.md` reutiliza intencionadamente el `CHANGELOG.md` canónico en inglés. | -| 🇨🇳 Chino (`zh`) | 🔴 Esqueleto | 0 / 26 | Solo está configurado como destino de compilación. Cada página muestra la versión en inglés. | +| 🇨🇳 Chino (`zh`) | ✅ Completo | 26 / 26 | Todas las páginas del idioma están presentes. Phase 1: nivel superior + user-guide principal; Phase 2: user-guide restante + todos los tutorials; Phase 3: contributing + reference. `docs/zh/changelog.md` reutiliza intencionadamente el `CHANGELOG.md` canónico en inglés. | | 🇫🇷 Francés (`fr`) | ✅ Completo | 26 / 26 | Todas las páginas del idioma están presentes. Phase 1: nivel superior + núcleo del user-guide; Phase 2: resto del user-guide + todos los tutoriales; Phase 3: contributing + reference. `docs/fr/changelog.md` reutiliza intencionadamente el `CHANGELOG.md` canónico en inglés. | | 🇩🇪 Alemán (`de`) | ✅ Completo | 26 / 26 | Todas las páginas del idioma están presentes. Phase 1: nivel superior + núcleo del user-guide; Phase 2: resto del user-guide + todos los tutoriales; Phase 3: contributing + reference. `docs/de/changelog.md` reutiliza intencionadamente el `CHANGELOG.md` canónico en inglés. | -*Verificado el 2026-05-17; la fila de `de` se volvió a contar para la rama actual tras completar Phase 3 (contributing + reference). El alemán ya tiene todas las páginas del idioma presentes, mientras que `docs/de/changelog.md` apunta al changelog canónico en inglés.* Esta cuenta se mantiene a mano; para volver a contar el estado actual desde la raíz del repositorio, ejecuta: +*Verificado el 2026-06-18; la fila de `zh` se volvió a contar para la rama actual tras completar Phase 3 (contributing + reference). El chino ya tiene las 26 páginas del idioma presentes y se sigue con el estado ✅ Completo.* Esta cuenta se mantiene a mano; para volver a contar el estado actual desde la raíz del repositorio, ejecuta: ```console $ for loc in en ko ja zh es fr de; do diff --git a/docs/fr/reference/translation-status.md b/docs/fr/reference/translation-status.md index 7722be9..fc06101 100644 --- a/docs/fr/reference/translation-status.md +++ b/docs/fr/reference/translation-status.md @@ -21,12 +21,12 @@ Les nombres ci-dessous comptent les pages Markdown dans l'arborescence de chaque | 🇬🇧 Anglais (`en`) | ✅ Source de vérité | 26 / 26 | Référence. | | 🇰🇷 Coréen (`ko`) | ✅ Complet | 26 / 26 | Toutes les pages de la locale sont présentes. Phase 1 : top-level + cœur du guide de l'utilisateur ; Phase 2 : reste du guide de l'utilisateur + tous les tutoriels ; Phase 3 : pages de contribution + de référence. `docs/ko/changelog.md` réutilise intentionnellement le `CHANGELOG.md` canonique en anglais. | | 🇯🇵 Japonais (`ja`) | ✅ Complet | 26 / 26 | Toutes les pages de la locale sont présentes. Phase 1 : top-level + cœur du guide de l'utilisateur ; Phase 2 : reste du guide de l'utilisateur + tous les tutoriels ; Phase 3 : pages de contribution + de référence. `docs/ja/changelog.md` réutilise intentionnellement le `CHANGELOG.md` canonique en anglais. | -| 🇨🇳 Chinois (`zh`) | 🔴 Squelette | 0 / 26 | Cible de build uniquement. Toutes les pages retombent sur l'anglais. | +| 🇨🇳 Chinois (`zh`) | ✅ Complet | 26 / 26 | Toutes les pages de la locale sont présentes. Phase 1 : top-level + cœur du guide de l'utilisateur ; Phase 2 : reste du guide de l'utilisateur + tous les tutoriels ; Phase 3 : pages de contribution + de référence. `docs/zh/changelog.md` réutilise intentionnellement le `CHANGELOG.md` canonique en anglais. | | 🇪🇸 Espagnol (`es`) | ✅ Complet | 26 / 26 | Toutes les pages de la locale sont présentes. Phase 1 : top-level + cœur du guide de l'utilisateur ; Phase 2 : reste du guide de l'utilisateur + tous les tutoriels ; Phase 3 : pages de contribution + de référence. `docs/es/changelog.md` réutilise intentionnellement le `CHANGELOG.md` canonique en anglais. | | 🇫🇷 Français (`fr`) | ✅ Complet | 26 / 26 | Toutes les pages de la locale sont présentes. Phase 1 : top-level + cœur du guide de l'utilisateur ; Phase 2 : reste du guide de l'utilisateur + tous les tutoriels ; Phase 3 : pages de contribution + de référence. `docs/fr/changelog.md` réutilise intentionnellement le `CHANGELOG.md` canonique en anglais. | | 🇩🇪 Allemand (`de`) | ✅ Complet | 26 / 26 | Toutes les pages de la locale sont présentes. Phase 1 : top-level + cœur du guide de l'utilisateur ; Phase 2 : reste du guide de l'utilisateur + tous les tutoriels ; Phase 3 : contributing + reference. `docs/de/changelog.md` réutilise intentionnellement le `CHANGELOG.md` canonique en anglais. | -*Instantané vérifié le 2026-05-17 ; la ligne `de` a été recomptée sur la branche actuelle après la fin de la Phase 3 (contributing + reference). L'allemand comporte désormais toutes les pages de la locale et `docs/de/changelog.md` pointe vers le changelog canonique en anglais.* Ces compteurs sont maintenus à la main ; pour recompter l'état actuel depuis la racine du dépôt, exécutez : +*Instantané vérifié le 2026-06-18 ; la ligne `zh` a été recomptée sur la branche actuelle après la fin de la Phase 3 (pages de contribution + de référence). Le chinois comporte désormais l'ensemble des 26 pages traduites et est suivi en ✅ Complet.* Ces compteurs sont maintenus à la main ; pour recompter l'état actuel depuis la racine du dépôt, exécutez : ```console $ for loc in en ko ja zh es fr de; do diff --git a/docs/ja/reference/translation-status.md b/docs/ja/reference/translation-status.md index 41223fb..279ada4 100644 --- a/docs/ja/reference/translation-status.md +++ b/docs/ja/reference/translation-status.md @@ -21,12 +21,12 @@ FastAPI-fastkit のドキュメントは複数の言語でビルドされます | 🇬🇧 English (`en`) | ✅ 原典 (Source of truth) | 26 / 26 | すべての基準になる原文です。 | | 🇰🇷 Korean (`ko`) | ✅ 完了 | 26 / 26 | ロケール側のページはすべてそろっています。Phase 1: トップレベル + コアの user-guide、Phase 2: 残りの user-guide + すべての tutorial、Phase 3: contributing + reference。`docs/ko/changelog.md` は英語の `CHANGELOG.md` を意図的に再利用しています。 | | 🇯🇵 Japanese (`ja`) | ✅ 完了 | 26 / 26 | ロケール側のページはすべて存在します。Phase 1: トップレベル + コアの user-guide、Phase 2: 残りの user-guide + すべての tutorial、Phase 3: contributing + reference。`docs/ja/changelog.md` は英語の `CHANGELOG.md` を意図的に再利用しています。 | -| 🇨🇳 Chinese (`zh`) | 🔴 スケルトン | 0 / 26 | ビルドターゲットのみ。すべてのページが英語にフォールバックします。 | +| 🇨🇳 Chinese (`zh`) | ✅ 完了 | 26 / 26 | ロケール側のページはすべて存在します。Phase 1: トップレベル + コアの user-guide、Phase 2: 残りの user-guide + すべての tutorial、Phase 3: contributing + reference。`docs/zh/changelog.md` は英語の `CHANGELOG.md` を意図的に再利用しています。 | | 🇪🇸 Spanish (`es`) | ✅ 完了 | 26 / 26 | ロケール側のページはすべて存在します。Phase 1: トップレベル + コアの user-guide、Phase 2: 残りの user-guide + すべての tutorial、Phase 3: contributing + reference。`docs/es/changelog.md` は英語の `CHANGELOG.md` を意図的に再利用しています。 | | 🇫🇷 French (`fr`) | ✅ 完了 | 26 / 26 | ロケール側のページはすべて存在します。Phase 1: トップレベル + コアの user-guide、Phase 2: 残りの user-guide + すべての tutorial、Phase 3: contributing + reference。`docs/fr/changelog.md` は英語の `CHANGELOG.md` を意図的に再利用しています。 | | 🇩🇪 German (`de`) | ✅ 完了 | 26 / 26 | ロケール側のページはすべて存在します。Phase 1: トップレベル + コアの user-guide、Phase 2: 残りの user-guide + すべての tutorial、Phase 3: contributing + reference。`docs/de/changelog.md` は英語の `CHANGELOG.md` を意図的に再利用しています。 | -*スナップショット検証日: 2026-05-17。Phase 3 (contributing + reference) を反映した現在のブランチを基準に `de` 行を再集計しました。ドイツ語はロケール側のページがすべてそろっており、`docs/de/changelog.md` は英語版 changelog をそのまま指しています。* この表は手動で管理されています。リポジトリルートで現在の状態を再カウントしたい場合は、次のコマンドを実行してください: +*スナップショット検証日: 2026-06-18。Phase 3 (contributing + reference) を反映した現在のブランチを基準に `zh` 行を再集計しました。中国語はロケール側のページが 26 件すべて存在し、✅ 完了として追跡されます。* この表は手動で管理されています。リポジトリルートで現在の状態を再カウントしたい場合は、次のコマンドを実行してください: ```console $ for loc in en ko ja zh es fr de; do diff --git a/docs/ko/reference/translation-status.md b/docs/ko/reference/translation-status.md index da1e35f..0f8c8fc 100644 --- a/docs/ko/reference/translation-status.md +++ b/docs/ko/reference/translation-status.md @@ -21,12 +21,12 @@ FastAPI-fastkit 문서는 여러 언어로 빌드되지만, 모든 번역이 ** | 🇬🇧 English (`en`) | ✅ 원본 | 26 / 26 | 기준이 되는 원문입니다. | | 🇰🇷 한국어 (`ko`) | ✅ 완료 | 26 / 26 | 언어별 페이지는 모두 존재합니다. Phase 1: 최상위 + 핵심 user-guide, Phase 2: 나머지 user-guide + 모든 tutorial, Phase 3: contributing + reference. `docs/ko/changelog.md` 는 영문 기준 `CHANGELOG.md` 를 그대로 사용합니다. | | 🇯🇵 일본어 (`ja`) | ✅ 완료 | 26 / 26 | 언어별 페이지는 모두 존재합니다. Phase 1: 최상위 + 핵심 user-guide, Phase 2: 나머지 user-guide + 모든 tutorial, Phase 3: contributing + reference. `docs/ja/changelog.md` 는 영문 기준 `CHANGELOG.md` 를 그대로 사용합니다. | -| 🇨🇳 중국어 (`zh`) | 🔴 기본 구조만 있음 | 0 / 26 | 빌드 대상만 설정되어 있으며, 모든 페이지는 영어 원문으로 표시됩니다. | +| 🇨🇳 중국어 (`zh`) | ✅ 완료 | 26 / 26 | 언어별 페이지는 모두 존재합니다. Phase 1: 최상위 + 핵심 user-guide, Phase 2: 나머지 user-guide + 모든 tutorial, Phase 3: contributing + reference. `docs/zh/changelog.md` 는 영문 기준 `CHANGELOG.md` 를 그대로 사용합니다. | | 🇪🇸 스페인어 (`es`) | ✅ 완료 | 26 / 26 | 언어별 페이지는 모두 존재합니다. Phase 1: 최상위 + 핵심 user-guide, Phase 2: 나머지 user-guide + 모든 tutorial, Phase 3: contributing + reference. `docs/es/changelog.md` 는 영문 기준 `CHANGELOG.md` 를 그대로 사용합니다. | | 🇫🇷 프랑스어 (`fr`) | ✅ 완료 | 26 / 26 | 모든 로케일 페이지가 존재합니다. Phase 1: 최상위 + 핵심 user-guide; Phase 2: 나머지 user-guide + 모든 tutorial; Phase 3: contributing + reference. `docs/fr/changelog.md` 는 영문 기준 `CHANGELOG.md` 를 그대로 재사용합니다. | | 🇩🇪 독일어 (`de`) | ✅ 완료 | 26 / 26 | 모든 로케일 페이지가 존재합니다. Phase 1: 최상위 + 핵심 user-guide; Phase 2: 나머지 user-guide + 모든 tutorial; Phase 3: contributing + reference. `docs/de/changelog.md` 는 영문 기준 `CHANGELOG.md` 를 그대로 재사용합니다. | -*스냅샷 검증 시점: 2026-05-17. Phase 3(contributing + reference) 작업이 반영된 현재 브랜치 기준으로 `de` 행을 다시 집계했습니다. 독일어는 이제 모든 로케일 페이지를 보유하며 `docs/de/changelog.md` 는 영문 기준 changelog를 그대로 가리킵니다.* 이 표는 수동으로 관리됩니다. 리포지토리 루트에서 현재 상태를 다시 세고 싶다면 다음 명령을 실행하세요: +*스냅샷 검증 시점: 2026-06-18. Phase 3(contributing + reference) 작업이 반영된 현재 브랜치 기준으로 `zh` 행을 다시 집계했습니다. 중국어는 이제 26개 로케일 페이지를 모두 보유하며 ✅ 완료 상태로 추적됩니다.* 이 표는 수동으로 관리됩니다. 리포지토리 루트에서 현재 상태를 다시 세고 싶다면 다음 명령을 실행하세요: ```console $ for loc in en ko ja zh es fr de; do diff --git a/docs/zh/changelog.md b/docs/zh/changelog.md new file mode 100644 index 0000000..f4d16fd --- /dev/null +++ b/docs/zh/changelog.md @@ -0,0 +1 @@ +{!CHANGELOG.md!} diff --git a/docs/zh/contributing/code-guidelines.md b/docs/zh/contributing/code-guidelines.md new file mode 100644 index 0000000..2a90636 --- /dev/null +++ b/docs/zh/contributing/code-guidelines.md @@ -0,0 +1,748 @@ +# 代码规范 + +为参与 FastAPI-fastkit 贡献而准备的完整编码标准与最佳实践。 + +## 总览 + +这些规范用于保证 FastAPI-fastkit 项目在代码质量、一致性与可维护性上的水准。遵循这些标准,可以让代码库更易于阅读、维护与扩展。 + +## Python 代码风格 + +### PEP 8 一致性 + +遵循 [PEP 8](https://www.python.org/dev/peps/pep-0008/),并按以下具体约定: + +- **行宽**:88 字符(Black 默认) +- **缩进**:4 空格(不要使用 tab) +- **末尾逗号**:多行结构必须保留末尾逗号 +- **字符串引号**:优先使用双引号 + +### 代码格式化 + +我们使用 **Black** 进行自动格式化: + +```python +# Good ✅ +def create_project( + name: str, + template: str, + options: Dict[str, Any], +) -> ProjectResult: + """Create a new FastAPI project.""" + return ProjectResult(name=name, template=template) + +# Bad ❌ +def create_project(name: str, template: str, options: Dict[str,Any])->ProjectResult: + """Create a new FastAPI project.""" + return ProjectResult(name=name,template=template) +``` + +### Import 组织 + +使用 **isort** 组织 import: + +```python +# Standard library imports +import os +import sys +from pathlib import Path +from typing import Dict, List, Optional, Union + +# Third-party imports +import click +import pydantic +from fastapi import FastAPI + +# Local imports +from fastapi_fastkit.commands import BaseCommand +from fastapi_fastkit.utils import validation +from fastapi_fastkit.templates.manager import TemplateManager +``` + +## 类型注解 + +### 必备的类型注解 + +所有公共函数与方法必须带有类型注解: + +```python +# Good ✅ +def validate_project_name(name: str) -> bool: + """Validate project name format.""" + return name.isidentifier() and not name.startswith('_') + +def create_files( + files: List[Path], + template_data: Dict[str, Any] +) -> List[Path]: + """Create files from template data.""" + created_files = [] + for file_path in files: + # Implementation... + created_files.append(file_path) + return created_files + +# Bad ❌ +def validate_project_name(name): + return name.isidentifier() and not name.startswith('_') +``` + +### 复杂类型注解 + +对复杂结构使用合适的类型注解: + +```python +from typing import Dict, List, Optional, Union, Tuple, Any +from pathlib import Path + +# Type aliases for complex types +ProjectConfig = Dict[str, Union[str, bool, List[str]]] +FileMapping = Dict[Path, str] +ValidationResult = Tuple[bool, Optional[str]] + +def process_template( + template_path: Path, + config: ProjectConfig, + output_dir: Optional[Path] = None, +) -> ValidationResult: + """Process template with configuration.""" + # Implementation... + return True, None +``` + +## 命名约定 + +### 变量与函数 + +- 变量与函数使用 **snake_case** +- 名称应**有描述性**,清晰表达用途 +- **避免缩写**,除非该缩写已被广泛接受 + +```python +# Good ✅ +project_name = "my-api" +template_directory = Path("templates") +user_input_data = get_user_input() + +def validate_email_address(email: str) -> bool: + """Validate email address format.""" + return "@" in email and "." in email + +# Bad ❌ +proj_nm = "my-api" +temp_dir = Path("templates") +usr_data = get_input() + +def validate_email(e): + return "@" in e and "." in e +``` + +### 类 + +- 类名使用 **PascalCase** +- 名称要**具体且具描述性** + +```python +# Good ✅ +class SomeClass: + """Represents example class of FastAPI-fastkit.""" + pass + +class SomeClassValidationError(Exception): + """Raised when example class validation fails.""" + pass + +class UserInputHandler: + """Handles user input validation and processing.""" + pass + +# Bad ❌ +class Class: + pass + +class Error(Exception): + pass + +class Handler: + pass +``` + +### 常量 + +- 使用带下划线的 **UPPER_CASE** +- 仅在**模块级**定义常量 + +```python +# Good ✅ +DEFAULT_TEMPLATE_NAME = "fastapi-default" +MAX_PROJECT_NAME_LENGTH = 50 +SUPPORTED_PYTHON_VERSIONS = ["3.8", "3.9", "3.10", "3.11", "3.12"] + +# Bad ❌ +default_template = "fastapi-default" +maxLength = 50 +versions = ["3.8", "3.9", "3.10", "3.11", "3.12"] +``` + +## 文档标准 + +### Docstring + +所有公共 API 使用 **Google 风格 docstring**: + +```python +def create_project_structure( + project_name: str, + template_path: Path, + output_directory: Optional[Path] = None, + overwrite: bool = False, +) -> List[Path]: + """Create project structure from template. + + Creates a new FastAPI project structure by copying and processing + template files. Supports variable substitution and file customization. + + Args: + project_name: Name of the project to create. Must be a valid + Python identifier. + template_path: Path to the template directory containing + source files and configuration. + output_directory: Directory where project will be created. + Defaults to current working directory. + overwrite: Whether to overwrite existing files. If False, + raises error when files exist. + + Returns: + List of created file paths in order of creation. + + Raises: + ValueError: If project_name is invalid or empty. + FileExistsError: If output directory exists and overwrite is False. + TemplateNotFoundError: If template_path doesn't exist. + PermissionError: If insufficient permissions to create files. + + Example: + ```python + template_path = Path("templates/fastapi-default") + created_files = create_project_structure( + project_name="my-api", + template_path=template_path, + output_directory=Path("./projects"), + overwrite=False + ) + print(f"Created {len(created_files)} files") + ``` + """ + # Implementation here... + pass +``` + +### 注释 + +- **解释 WHY,而不是 WHAT** +- **节制使用** —— 代码本身应具有自解释性 +- 代码改动后**同步更新注释** + +```python +# Good ✅ +def validate_dependencies(requirements: List[str]) -> bool: + """Validate project dependencies.""" + # Skip validation in development mode to allow experimental packages + if os.getenv("FASTKIT_DEV_MODE"): + return True + + # Check each requirement against known security vulnerabilities + for requirement in requirements: + if is_vulnerable_package(requirement): + return False + + return True + +# Bad ❌ +def validate_dependencies(requirements: List[str]) -> bool: + """Validate project dependencies.""" + # Check if dev mode + if os.getenv("FASTKIT_DEV_MODE"): + return True + + # Loop through requirements + for requirement in requirements: + # Check if vulnerable + if is_vulnerable_package(requirement): + return False + + # Return true + return True +``` + +## 错误处理 + +### 异常处理 + +- 尽量**捕获具体异常** +- **提供有意义的错误信息** +- **合理地记录错误日志** + +```python +# Good ✅ +def load_template_config(template_path: Path) -> Dict[str, Any]: + """Load template configuration from file.""" + config_file = template_path / "template.yaml" + + try: + with open(config_file, 'r') as f: + return yaml.safe_load(f) + except FileNotFoundError: + raise TemplateNotFoundError( + f"Template configuration not found: {config_file}" + ) + except yaml.YAMLError as e: + raise TemplateConfigError( + f"Invalid YAML syntax in {config_file}: {e}" + ) + except PermissionError: + raise TemplateAccessError( + f"Permission denied reading {config_file}" + ) + +# Bad ❌ +def load_template_config(template_path: Path) -> Dict[str, Any]: + """Load template configuration from file.""" + config_file = template_path / "template.yaml" + + try: + with open(config_file, 'r') as f: + return yaml.safe_load(f) + except Exception as e: + raise Exception(f"Error loading config: {e}") +``` + +### 自定义异常 + +为不同错误场景定义具体的异常类: + +```python +class FastKitError(Exception): + """Base exception for FastAPI-fastkit errors.""" + pass + +class ProjectCreationError(FastKitError): + """Raised when project creation fails.""" + pass + +class TemplateNotFoundError(FastKitError): + """Raised when template is not found.""" + pass + +class ValidationError(FastKitError): + """Raised when input validation fails.""" + + def __init__(self, message: str, field: str = None): + super().__init__(message) + self.field = field +``` + +## 测试标准 + +### 测试结构 + +测试要有清晰的结构与命名: + +```python +class TestProjectCreation: + """Test project creation functionality.""" + + def test_create_project_with_valid_name(self, tmp_path): + """Test project creation with valid project name.""" + project_name = "test-project" + result = create_project(project_name, template="minimal", output=tmp_path) + + assert result.success is True + assert (tmp_path / project_name).exists() + assert (tmp_path / project_name / "src" / "main.py").exists() + + def test_create_project_with_invalid_name(self): + """Test project creation fails with invalid name.""" + with pytest.raises(ValueError, match="Invalid project name"): + create_project("invalid-project-name!", template="minimal") + + def test_create_project_overwrites_existing(self, tmp_path): + """Test project creation overwrites existing directory when forced.""" + project_name = "existing-project" + project_dir = tmp_path / project_name + project_dir.mkdir() + + result = create_project( + project_name, + template="minimal", + output=tmp_path, + overwrite=True + ) + + assert result.success is True + assert project_dir.exists() +``` + +### 测试覆盖率 + +- 对新代码,**目标覆盖率 90% 以上** +- **覆盖边界情况**与异常路径 +- 对外部依赖使用 **mock** + +```python +def test_template_download_with_network_error(mock_requests): + """Test template download handles network errors gracefully.""" + mock_requests.get.side_effect = requests.ConnectionError("Network unreachable") + + with pytest.raises(TemplateDownloadError, match="Network error"): + download_template("https://example.com/template.zip") + +def test_file_creation_with_permission_error(mock_open): + """Test file creation handles permission errors.""" + mock_open.side_effect = PermissionError("Permission denied") + + with pytest.raises(FileCreationError, match="Permission denied"): + create_file(Path("/restricted/file.py"), content="test") +``` + +## Import 规范 + +### Import 组织 + +!!! note + + `isort` 格式化器会自动组织 import,只需运行 `bash scripts/format.sh` 即可。 + +1. **标准库** import 在最前 +2. **第三方** import 在中间 +3. **本地应用** import 在最后 +4. 每组之间用**空行**分隔 + +```python +# Standard library +import os +import sys +from pathlib import Path +from typing import Dict, List, Optional + +# Third-party +import click +import pydantic +import yaml +from fastapi import FastAPI + +# Local application +from fastapi_fastkit.commands.base import BaseCommand +from fastapi_fastkit.utils.validation import validate_project_name +from fastapi_fastkit.templates import TemplateManager +``` + +### Import 最佳实践 + +- **避免通配 import**(`from module import *`) +- 使用**绝对 import**,保持清晰 +- 当需要导入许多项时,**优先导入模块本身** + +```python +# Good ✅ +from fastapi_fastkit.utils import validation, files, formatting + +# Good ✅ (when importing few items) +from fastapi_fastkit.utils.validation import validate_email, validate_project_name + +# Bad ❌ +from fastapi_fastkit.utils.validation import * + +# Bad ❌ (when importing many items) +from fastapi_fastkit.utils.validation import ( + validate_email, validate_project_name, validate_template_name, + validate_dependencies, validate_python_version, validate_directory +) +``` + +## 安全规范 + +### 输入校验 + +始终对用户输入进行校验与净化: + +```python +def validate_project_name(name: str) -> str: + """Validate and sanitize project name.""" + if not name: + raise ValueError("Project name cannot be empty") + + if not name.isidentifier(): + raise ValueError("Project name must be a valid Python identifier") + + if name.startswith('_'): + raise ValueError("Project name cannot start with underscore") + + if len(name) > 50: + raise ValueError("Project name too long (max 50 characters)") + + # Sanitize by removing dangerous characters + sanitized = re.sub(r'[^a-zA-Z0-9_-]', '', name) + + return sanitized +``` + +### 文件操作 + +谨慎处理文件路径与文件操作: + +```python +def create_file_safely(file_path: Path, content: str, base_dir: Path) -> None: + """Create file safely within base directory.""" + # Resolve to prevent directory traversal attacks + resolved_path = file_path.resolve() + resolved_base = base_dir.resolve() + + # Ensure file is within base directory + try: + resolved_path.relative_to(resolved_base) + except ValueError: + raise SecurityError(f"File path outside base directory: {file_path}") + + # Create parent directories safely + resolved_path.parent.mkdir(parents=True, exist_ok=True) + + # Write file with appropriate permissions + resolved_path.write_text(content, encoding='utf-8') + resolved_path.chmod(0o644) # Read/write for owner, read for others +``` + +## 性能规范 + +### 高效的代码写法 + +- 对大数据集**使用生成器** +- **避免过早优化** +- **先剖析,再优化** + +```python +# Good ✅ - Generator for memory efficiency +def process_large_template(template_files: List[Path]) -> Iterator[ProcessedFile]: + """Process template files efficiently.""" + for file_path in template_files: + content = file_path.read_text() + processed_content = process_template_content(content) + yield ProcessedFile(path=file_path, content=processed_content) + +# Bad ❌ - Loads everything into memory +def process_large_template(template_files: List[Path]) -> List[ProcessedFile]: + """Process template files.""" + results = [] + for file_path in template_files: + content = file_path.read_text() + processed_content = process_template_content(content) + results.append(ProcessedFile(path=file_path, content=processed_content)) + return results +``` + +### 缓存 + +对开销大的操作使用缓存: + +```python +from functools import lru_cache + +@lru_cache(maxsize=128) +def get_template_metadata(template_path: Path) -> TemplateMetadata: + """Get template metadata with caching.""" + config_file = template_path / "template.yaml" + + if not config_file.exists(): + return TemplateMetadata(name=template_path.name) + + config = yaml.safe_load(config_file.read_text()) + return TemplateMetadata.from_config(config) +``` + +## Git 提交规范 + +### 提交信息格式 + +使用约定式提交(Conventional Commits)格式: + +``` +type(scope): description + +[optional body] + +[optional footer] +``` + +### 提交类型 + +- **feat**:新特性 +- **fix**:bug 修复 +- **docs**:文档相关改动 +- **style**:代码风格改动(格式化等) +- **refactor**:代码重构 +- **test**:增加或调整测试 +- **chore**:维护性任务 + +### 示例 + +```bash +# Good ✅ +feat(cli): add template validation command + +Add new command to validate template structure and configuration. +The command checks for required files, validates YAML syntax, +and ensures template follows conventions. + +Closes #123 + +# Good ✅ +fix(templates): handle missing dependency files gracefully + +When a template references a requirements file that doesn't exist, +show a clear error message instead of crashing. + +# Bad ❌ +update stuff + +# Bad ❌ +Fixed bug +``` + +## 代码评审规范 + +### 对作者而言 + +提交评审前请确认: + +1. **运行全部测试**,且全部通过 +2. **核查测试覆盖率**未下滑 +3. **必要时更新文档** +4. **遵循提交信息**规范 +5. **保持 PR 聚焦且小** + +### 对评审者而言 + +评审时请关注: + +1. **功能是否正确** —— 行为是否符合预期? +2. **测试是否完善** —— 是否覆盖边界情况? +3. **文档是否同步** —— 描述是否清晰、最新? +4. **代码风格** —— 是否遵循项目约定? +5. **是否考虑安全** —— 是否存在潜在漏洞? + +### 评审清单 + +- [ ] 代码符合风格规范 +- [ ] 测试齐备且全部通过 +- [ ] 文档已更新 +- [ ] 无安全漏洞 +- [ ] 已考虑性能问题 +- [ ] 错误处理得当 +- [ ] 提交信息符合规范 + +## 工具与自动化 + +### Pre-commit 钩子 + +我们使用 pre-commit 钩子来强制执行标准: + +```yaml +# .pre-commit-config.yaml +repos: +- repo: https://github.com/pre-commit/pre-commit-hooks + rev: v6.0.0 + hooks: + - id: trailing-whitespace + - id: end-of-file-fixer + - id: check-yaml + - id: check-toml + +- repo: local + hooks: + - id: format + name: format + entry: black --config pyproject.toml --check . + language: python + types: [python] + additional_dependencies: ['black>=24.10.0'] + pass_filenames: false + + - id: isort-check + name: isort check + entry: isort --sp pyproject.toml --check-only --diff . + language: python + types: [python] + additional_dependencies: ['isort>=5.13.2'] + pass_filenames: false + + - id: isort-fix + name: isort fix + entry: isort --sp pyproject.toml . + language: python + types: [python] + additional_dependencies: ['isort>=5.13.2'] + pass_filenames: false + + - id: black-fix + name: black fix + entry: black --config pyproject.toml . + language: python + types: [python] + additional_dependencies: ['black>=24.10.0'] + pass_filenames: false + + - id: mypy + name: mypy + entry: mypy --config-file pyproject.toml src + language: python + types: [python] + additional_dependencies: + - mypy>=1.12.0 + - rich>=13.9.2 + - click>=8.1.7 + - pyyaml>=6.0.0 + - types-PyYAML>=6.0.12 + pass_filenames: false + +ci: + autofix_commit_msg: 🎨 [pre-commit.ci] Auto format from pre-commit.com hooks + autoupdate_commit_msg: ⬆ [pre-commit.ci] pre-commit autoupdate +``` + +!!! note + + Pre-commit 钩子使用隔离的 Python 环境(`language: python`)。 + +### IDE 配置 + +推荐的 VS Code 设置: + +```json +{ + "python.linting.enabled": true, + "python.linting.mypyEnabled": true, + "python.formatting.provider": "black", + "python.sortImports.path": "isort", + "editor.formatOnSave": true, + "editor.codeActionsOnSave": { + "source.organizeImports": true + } +} +``` + +## 下一步 + +读完这些规范后: + +1. **配置开发环境**,参见 [开发环境配置](development-setup.md) +2. **从小贡献开始练手**,熟悉流程 +3. **在 GitHub Discussions 提问**,有不明之处随时问 +4. **阅读已有代码**,看看这些规范在实践中的样子 + +!!! tip "速查" + - 用 `make check-all` 验证您的代码是否满足全部规范 + - 配置 pre-commit 钩子,尽早发现问题 + - 拿不准时,看现有代码作为示例 + - 评审中遇到问题,不要犹豫,主动求助 + +遵循这些规范有助于让 FastAPI-fastkit 保持高水准的代码质量,并让所有人协作起来更轻松!🚀 diff --git a/docs/zh/contributing/development-setup.md b/docs/zh/contributing/development-setup.md new file mode 100644 index 0000000..96aed02 --- /dev/null +++ b/docs/zh/contributing/development-setup.md @@ -0,0 +1,816 @@ +# 开发环境配置 + +为参与 FastAPI-fastkit 贡献而准备开发环境的完整指南。 + +## 前置条件 + +开始之前,请确认您已具备: + +- 已安装 **Python 3.12 及以上** +- 已安装并配置 **Git** +- 具备 **Python 与 FastAPI 的基础知识** +- 一个 **文本编辑器或 IDE**(VS Code、PyCharm 等) + +## 借助 Makefile 快速配置 + +FastAPI-fastkit 提供了用于简化开发环境配置的 Makefile: + +
+ +```console +$ git clone https://github.com/bnbong/FastAPI-fastkit.git +$ cd FastAPI-fastkit +$ make install-dev +Setting up development environment... +Creating virtual environment... +Installing dependencies... +Installing pre-commit hooks... +✅ Development environment ready! +``` + +
+ +这一条命令会: + +- 以可编辑模式安装包及其开发依赖 +- 安装 pre-commit 钩子 +- 配置开发工具 + +!!! note + + 在运行该命令前,建议先创建并激活虚拟环境。 + +## 手动配置 + +如果您倾向于手动配置,或 Makefile 在您的系统上无法正常工作: + +### 1. 克隆仓库 + +
+ +```console +$ git clone https://github.com/bnbong/FastAPI-fastkit.git +$ cd FastAPI-fastkit +``` + +
+ +### 2. 创建虚拟环境 + +
+ +```console +$ python -m venv .venv +$ source .venv/bin/activate # On Windows: .venv\Scripts\activate +``` + +
+ +### 3. 安装依赖 + +
+ +```console +# Install package in editable mode with development dependencies +$ pip install -e ".[dev]" + +# Or install from requirements files +$ pip install -r requirements.txt +$ pip install -r requirements-dev.txt +``` + +
+ +### 4. 配置 pre-commit 钩子 + +
+ +```console +$ pre-commit install +pre-commit installed at .git/hooks/pre-commit +``` + +
+ +### 5. 验证安装 + +
+ +```console +$ fastkit --version +fastapi-fastkit, version 1.2.1 + +$ python -m pytest tests/ +======================== test session starts ======================== +collected 45 items +tests/test_cli.py::test_init_command PASSED +tests/test_templates.py::test_template_listing PASSED +... +======================== 45 passed in 2.34s ======================== +``` + +
+ +## 开发工具 + +开发环境包含多种工具,用于维护代码质量: + +### 一行命令 + +使用 Makefile: + +```console +$ make format lint +Running isort... +Running black... +Running mypy... +✅ All checks passed! +``` + +使用项目自带脚本: + +```console +$ ./scripts/format.sh +$ ./scripts/lint.sh +``` + +### 代码格式化 + +**Black** —— 代码格式化器: + +
+ +```console +$ black src/ tests/ +reformatted src/main.py +reformatted tests/test_cli.py +All done! ✨ 🍰 ✨ +``` + +
+ +**isort** —— import 排序: + +
+ +```console +$ isort src/ tests/ +Fixing import order in src/main.py +``` + +
+ +### 代码静态检查 + +**mypy** —— 类型检查: + +
+ +```console +$ mypy src/ +Success: no issues found in 12 source files +``` + +
+ +## 可用的 Make 命令 + +项目的 Makefile 为常见的开发任务提供了便捷命令: + +### 安装命令 + +| 命令 | 描述 | +|---------|-------------| +| `make install` | 以生产模式安装包 | +| `make install-dev` | 以包含开发依赖的方式安装包 | +| `make install-test` | 为测试用途安装包(卸载并重新安装) | +| `make uninstall` | 卸载包 | +| `make clean` | 清理构建产物与缓存文件 | + +### 代码质量命令 + +| 命令 | 描述 | +|---------|-------------| +| `make format` | 使用 black 与 isort 格式化代码 | +| `make format-check` | 检查代码格式,但不做修改 | +| `make lint` | 运行所有静态检查(isort、black、mypy) | + +### 测试命令 + +| 命令 | 描述 | +|---------|-------------| +| `make test` | 运行全部测试 | +| `make test-verbose` | 以详细模式运行测试 | +| `make test-coverage` | 运行测试并生成覆盖率报告 | +| `make coverage-report` | 生成详细的覆盖率报告(FORMAT=html/xml/json/all) | + +### 模板检查命令 + +| 命令 | 描述 | +|---------|-------------| +| `make inspect-templates` | 对所有模板执行模板检查 | +| `make inspect-templates-verbose` | 以详细输出运行模板检查 | +| `make inspect-template` | 检查指定模板(TEMPLATES 参数) | + +### 文档命令 + +| 命令 | 描述 | +|---------|-------------| +| `make serve-docs` | 在本地启动文档服务 | +| `make build-docs` | 构建文档 | + +### 翻译命令 + +| 命令 | 描述 | +|---------|-----------| +| `make translate` | 翻译文档(LANG、PROVIDER、MODEL 参数) | + +### 示例 + +
+ +```console +# Format code and run all checks +$ make format lint +Running isort... +Running black... +Running mypy... +✅ All checks passed! + +# Run tests with coverage +$ make test-coverage +======================== test session starts ======================== +collected 45 items +tests/test_cli.py::test_init_command PASSED +... +======================== 45 passed in 2.34s ======================== + +---------- coverage: platform darwin, python 3.12.1-final-0 ---------- +Name Stmts Miss Cover +-------------------------------------------- +src/main.py 45 2 96% +src/cli.py 89 5 94% +src/templates.py 67 3 96% +-------------------------------------------- +TOTAL 201 10 95% + +# Generate HTML coverage report +$ make coverage-report FORMAT=html +🌐 Opening HTML coverage report in browser... + +# Translate documentation to Korean +$ make translate LANG=ko PROVIDER=github MODEL=gpt-4o-mini +Starting translation... +Running: python scripts/translate.py --target-lang ko --api-provider github --model gpt-4o-mini +``` + +
+ +## 项目结构 + +理解项目结构对开发工作至关重要: + +```bash +FastAPI-fastkit/ +├── src/ +│ ├── fastapi_fastkit/ +│ │ ├── __main__.py # Entry point of the application +│ │ ├── backend/ +│ │ │ ├── inspector.py # FastAPI-fastkit template inspector +│ │ │ ├── interactive/ +│ │ │ │ ├── config_builder.py # Configuration builder for interactive mode +│ │ │ │ ├── prompts.py # Prompts for interactive mode +│ │ │ │ ├── selectors.py # Selectors logic for interactive mode +│ │ │ │ └── validators.py # User input validators for interactive mode +│ │ │ ├── main.py # Backend's logic entry point +│ │ │ ├── package_managers/ +│ │ │ │ ├── base.py # Base class for package managers +│ │ │ │ ├── factory.py # Factory for package managers +│ │ │ │ ├── pdm_manager.py # PDM package manager +│ │ │ │ ├── pip_manager.py # pip package manager +│ │ │ │ ├── poetry_manager.py # Poetry package manager +│ │ │ │ └── uv_manager.py # uv package manager +│ │ │ ├── project_builder/ +│ │ │ │ ├── config_generator.py # Configuration generator for project builder +│ │ │ │ └── dependency_collector.py # Dependency collector for project builder +│ │ │ └── transducer.py # Transducer for project builder +│ │ ├── cli.py # FastAPI-fastkit main CLI entry point +│ │ ├── core/ +│ │ │ ├── exceptions.py # Exception handling +│ │ │ └── settings.py # Settings configuration +│ │ ├── fastapi_project_template/ +│ │ │ ├── PROJECT_README_TEMPLATE.md # fastkit template project base README file +│ │ │ ├── README.md # fastkit template README +│ │ │ ├── fastapi-async-crud/ +│ │ │ ├── fastapi-custom-response/ +│ │ │ ├── fastapi-default/ +│ │ │ ├── fastapi-dockerized/ +│ │ │ ├── fastapi-empty/ +│ │ │ ├── fastapi-mcp/ +│ │ │ ├── fastapi-psql-orm/ +│ │ │ ├── fastapi-single-module/ +│ │ │ └── modules/ +│ │ │ ├── api/ +│ │ │ │ └── routes/ +│ │ │ ├── crud/ +│ │ │ └── schemas/ +│ │ ├── py.typed +│ │ └── utils/ +│ │ ├── logging.py # Logging configuration +│ │ └── main.py # FastAPI-fastkit main entry point +│ └── logs +├── tests +│ ├── conftest.py # pytest configuration +│ ├── test_backends/ +│ ├── test_cli_operations/ +│ ├── test_core.py +│ ├── test_rich/ +│ ├── test_templates/ +│ └── test_utils.py +├── uv.lock +├── docs/ # Documentation +├── scripts/ # Development scripts +├── mkdocs.yml +├── overrides/ # mkdocs overrides +├── pdm.lock +├── pyproject.toml +├── requirements-docs.txt # requirements for documentation +├── requirements.txt # requirements for development +├── CHANGELOG.md +├── CITATION.cff +├── CODE_OF_CONDUCT.md +├── CONTRIBUTING.md +├── LICENSE +├── MANIFEST.in +├── Makefile +├── README.md +├── SECURITY.md +└── env.example # environment example(configures translation AI model env vars) +``` + +### 关键目录 + +- **`src/fastapi_fastkit/`** —— 主包源码 + - **`cli.py`** —— 主 CLI 入口 + - **`backend/`** —— 核心后端逻辑 + - **`inspector.py`** —— 模板检查器 + - **`interactive/`** —— 交互模式相关组件(提示、选择器、校验器) + - **`package_managers/`** —— 包管理器实现(pip、uv、pdm、poetry) + - **`project_builder/`** —— 项目构建工具 + - **`transducer.py`** —— 模板 transducer + - **`core/`** —— 核心配置与异常 + - **`fastapi_project_template/`** —— 项目模板(fastapi-default、fastapi-async-crud 等) + - **`utils/`** —— 共享工具函数 +- **`tests/`** —— 测试套件 + - **`test_backends/`** —— 针对后端的测试 + - **`test_cli_operations/`** —— CLI 操作的测试 + - **`test_templates/`** —— 模板系统的测试 +- **`docs/`** —— 文档(MkDocs) + - 用户指南、教程与 API 参考 + +## 开发工作流 + +### 1. 创建特性分支 + +
+ +```console +$ git checkout -b feature/add-new-template +Switched to a new branch 'feature/add-new-template' +``` + +
+ +### 2. 进行改动 + +编辑代码、添加特性、修复 bug…… + +### 3. 运行测试与检查 + +
+ +```console +$ make dev-check +Running all quality checks... +Running all tests... +✅ All tests passed! +``` + +
+ +### 4. 提交改动 + +pre-commit 钩子会自动运行: + +
+ +```console +$ git add . +$ git commit -m "Add new FastAPI template with authentication" +format...................................................................Passed +isort-check..............................................................Passed +black-fix................................................................Passed +mypy.....................................................................Passed +[feature/add-new-template abc1234] Add new FastAPI template with authentication +``` + +
+ +### 5. 推送并创建 Pull Request + +
+ +```console +$ git push origin feature/add-new-template +$ gh pr create --title "Add new FastAPI template with authentication" +``` + +
+ +## 测试 + +### 运行测试 + +**全部测试:** + +
+ +```console +$ make test +# or +$ python -m pytest +``` + +
+ +**指定测试文件:** + +
+ +```console +$ python -m pytest tests/test_cli.py -v +``` + +
+ +**带覆盖率:** + +
+ +```console +$ make test-coverage +# or +$ python -m pytest --cov=src --cov-report=html +``` + +
+ +### 编写测试 + +新增特性时,务必同步编写测试: + +```python +# tests/test_commands/test_new_feature.py +import pytest +from fastapi_fastkit.commands.new_feature import NewFeatureCommand + +class TestNewFeatureCommand: + def test_command_success(self): + """Test successful command execution""" + command = NewFeatureCommand() + result = command.execute(valid_args) + assert result.success is True + assert result.message == "Feature executed successfully" + + def test_command_validation_error(self): + """Test command with invalid arguments""" + command = NewFeatureCommand() + with pytest.raises(ValueError, match="Invalid argument"): + command.execute(invalid_args) + + def test_command_edge_case(self): + """Test edge case handling""" + command = NewFeatureCommand() + result = command.execute(edge_case_args) + assert result.success is True + assert "warning" in result.message.lower() +``` + +### 测试分类 + +**单元测试** —— 测试单个函数与类: + +```python +def test_validate_project_name(): + assert validate_project_name("valid-name") is True + assert validate_project_name("invalid name!") is False +``` + +**集成测试** —— 测试命令之间的交互: + +```python +def test_init_command_creates_project(tmp_path): + result = runner.invoke(cli, ['init'], input='test-project\n...') + assert result.exit_code == 0 + assert (tmp_path / "test-project").exists() +``` + +**端到端测试** —— 测试完整的工作流: + +```python +def test_full_project_creation_workflow(tmp_path): + # Create project + result = runner.invoke(cli, ['init'], input='...') + assert result.exit_code == 0 + + # Add route + result = runner.invoke(cli, ['addroute', 'test-project', 'users']) + assert result.exit_code == 0 + + # Verify files exist + assert (tmp_path / "test-project" / "src" / "api" / "routes" / "users.py").exists() +``` + +## 文档 + +### 在本地启动文档服务 + +
+ +```console +$ make serve-docs +INFO - Building documentation... +INFO - Cleaning site directory +INFO - Documentation built in 0.43 seconds +INFO - [14:30:00] Serving on http://127.0.0.1:8000/ +``` + +
+ +### 构建文档 + +
+ +```console +$ make build-docs +INFO - Building documentation... +INFO - Documentation built in 0.43 seconds +``` + +
+ +### 编写文档 + +文档使用 Markdown 编写,由 MkDocs 构建。以下是示例结构: + +**特性指南模板:** + +````markdown +# New Feature Guide + +This guide explains how to use the new feature. + +## Prerequisites + +- FastAPI-fastkit installed +- Basic Python knowledge + +## Usage + +
+ +```console +$ fastkit new-feature --option value +✅ Feature executed successfully! +``` + +
+ +!!! tip "Pro Tip" + Use `--help` to see all available options. +```` + +要了解 `mkdocs-material` 的详细参考,请查看 [mkdocs-material 文档](https://squidfunk.github.io/mkdocs-material/reference/admonitions/)。 + +## 代码风格指南 + +### Python 代码风格 + +遵循 [PEP 8](https://www.python.org/dev/peps/pep-0008/),并附加以下具体约定: + +- **行宽**:88 字符(Black 默认) +- **import**:由 isort 组织 +- **类型注解**:所有公共函数都必须提供 +- **docstring**:所有公共 API 使用 Google 风格 + +### 示例 + +```python +from typing import List, Optional +from pathlib import Path + +def create_project_structure( + project_name: str, + template_path: Path, + output_dir: Optional[Path] = None, +) -> List[Path]: + """Create project structure from template. + + Args: + project_name: Name of the project to create + template_path: Path to the template directory + output_dir: Output directory, defaults to current directory + + Returns: + List of created file paths + + Raises: + ValueError: If project_name is invalid + FileNotFoundError: If template_path doesn't exist + """ + if not project_name.isidentifier(): + raise ValueError(f"Invalid project name: {project_name}") + + if not template_path.exists(): + raise FileNotFoundError(f"Template not found: {template_path}") + + # Implementation here... + return created_files +``` + +## 环境变量 + +进行开发时,您可以设置以下环境变量: + +| 变量 | 描述 | 默认值 | +|----------|-------------|---------| +| `FASTKIT_DEBUG` | 启用调试日志 | `False` | +| `FASTKIT_DEV_MODE` | 启用开发模式特性 | `False` | +| `FASTKIT_TEMPLATE_DIR` | 自定义模板目录 | 内置模板 | +| `FASTKIT_CONFIG_DIR` | 配置目录 | `~/.fastkit` | +| `TRANSLATION_API_KEY` | 翻译 API key(使用 [Github AI 模型提供方](https://github.com/marketplace/models/azure-openai) 时填写 Github PAT) | `None` | + +
+ +```console +$ export FASTKIT_DEBUG=true +$ export FASTKIT_DEV_MODE=true +$ fastkit init +DEBUG: Loading configuration from /home/user/.fastkit/ +DEBUG: Available templates: ['fastapi-default', ...] +``` + +
+ +其他环境变量设置请参阅 [@settings.py](https://github.com/bnbong/FastAPI-fastkit/blob/main/src/fastapi_fastkit/core/settings.py) 模块。 + +## 故障排查 + +### 常见问题 + +**1. pre-commit 钩子失败:** + +
+ +```console +$ git commit -m "Fix bug" +black....................................................................Failed +hookid: black + +Files were modified by this hook. Additional output: + +would reformat src/cli.py +``` + +
+ +**解决办法:** 运行格式化器后再次提交: + +
+ +```console +$ make format +$ git add . +$ git commit -m "Fix bug" +``` + +
+ +**2. 在不同 Python 版本上测试失败:** + +**解决办法:** 使用 tox 在多个 Python 版本上测试: + +
+ +```console +$ pip install tox +$ tox +py38: commands succeeded +py39: commands succeeded +py310: commands succeeded +py311: commands succeeded +py312: commands succeeded +``` + +
+ +**3. 开发时出现 import 错误:** + +**解决办法:** 以可编辑模式安装包: +
+ +```console +$ pip install -e . +``` + +
+ +### 获取帮助 + +- **[GitHub Issues](https://github.com/bnbong/FastAPI-fastkit/issues)**:报告 bug 与请求特性 +- **[GitHub Discussions](https://github.com/bnbong/FastAPI-fastkit/discussions)**:提问与分享想法 +- **文档**:查看 [用户指南](../user-guide/installation.md) + +## 贡献指南 + +### 提交 PR 之前 + +1. **运行所有检查:** `make dev-check` +2. **必要时更新文档** +3. **为新特性增加测试** +4. **遵循提交信息规范** + +### 提交信息格式 + +``` +type(scope): brief description + +Longer description if needed + +Fixes #123 +``` + +**类型(type):** + +- `feat`:新特性 +- `fix`:bug 修复 +- `docs`:文档相关改动 +- `style`:代码风格改动 +- `refactor`:代码重构 +- `test`:增加 / 调整测试 +- `chore`:维护性任务 + +**示例:** + +``` +feat(cli): add new template command + +Add support for creating projects from custom templates. +The command accepts a template path and creates a new +project with the specified configuration. + +Fixes #45 + +fix(templates): handle missing template files gracefully + +When a template file is missing, show a clear error message +instead of crashing with a stack trace. + +Fixes #67 +``` + +## 发布流程 + +对维护者而言,发布流程如下: + +1. **更新版本号**(`setup.py` 与 `__init__.py`) +2. **更新 CHANGELOG.md** +3. **创建发布 PR** +4. **合并后打 tag** +5. **GitHub Actions** 自动构建并发布 + +
+ +```console +$ git tag v1.2.0 +$ git push origin v1.2.0 +``` + +
+ +## 下一步 + +开发环境已就绪后: + +1. **[浏览代码库](https://github.com/bnbong/FastAPI-fastkit/tree/main/src/fastapi_fastkit)** —— 理解整体架构 +2. **运行测试套件**,确认一切正常 +3. **从 GitHub 上挑选一个 [issue](https://github.com/bnbong/FastAPI-fastkit/issues)** 着手开发 +4. **加入 [discussions](https://github.com/bnbong/FastAPI-fastkit/discussions)**,与其他贡献者交流 + +祝您编码愉快!🚀 + +!!! tip "开发小贴士" + - 提交前先运行 `make dev-check` + - 优先编写测试(TDD 方式) + - 让每次提交保持小而聚焦 + - 新特性同时更新文档 diff --git a/docs/zh/contributing/template-creation-guide.md b/docs/zh/contributing/template-creation-guide.md new file mode 100644 index 0000000..ae3ebe9 --- /dev/null +++ b/docs/zh/contributing/template-creation-guide.md @@ -0,0 +1,575 @@ +# FastAPI 模板创建指南 + +为 FastAPI-fastkit 添加新 FastAPI 项目模板的完整指南。 + +## 🎯 总览 + +添加新模板分为 5 个步骤: + +1. **📋 规划与设计** —— 明确模板用途与结构 +2. **🏗️ 模板实现** —— 创建必需的结构与文件 +3. **🔍 本地校验** —— 用 inspector 校验模板 +4. **📚 文档** —— 编写 README 与使用指南 +5. **🚀 提交与评审** —— 创建 PR 并由社区评审 + +## 📋 第 1 步:规划与设计 + +### 明确模板用途 + +在创建新模板之前,请回答以下问题: + +- **该模板的独特价值是什么?** +- **它与现有模板的差异在哪里?** +- **目标用户群体是谁?** +- **会包含怎样的技术栈?** + +### 模板命名规范 + +``` +fastapi-{purpose}-{stack} +``` + +示例: + +- `fastapi-microservice`(微服务模板) +- `fastapi-graphql`(GraphQL 集成模板) +- `fastapi-auth-jwt`(JWT 认证模板) + +### 技术栈规划 + +事先定义好要包含的主要技术: + +```yaml +# Example: fastapi-microservice template +core_dependencies: + - fastapi + - uvicorn + - pydantic + - pydantic-settings + +additional_features: + - sqlalchemy (ORM) + - alembic (migrations) + - redis (caching) + - celery (background tasks) + - pytest (testing) + +development_tools: + - black (code formatting) + - isort (import sorting) + - mypy (type checking) + - pre-commit (Git hooks) +``` + +## 🏗️ 第 2 步:模板实现 + +### 必需的目录结构 + +``` +fastapi-{template-name}/ +├── src/ # Application source code +│ ├── main.py-tpl # ✅ FastAPI app entry point (required) +│ ├── __init__.py-tpl +│ ├── api/ # API routers +│ │ ├── __init__.py-tpl +│ │ ├── api.py-tpl # Main API router +│ │ └── routes/ # Individual routes +│ │ ├── __init__.py-tpl +│ │ └── items.py-tpl # Example route +│ ├── core/ # Core configuration +│ │ ├── __init__.py-tpl +│ │ └── config.py-tpl # Settings management +│ ├── crud/ # CRUD logic +│ │ ├── __init__.py-tpl +│ │ └── items.py-tpl +│ ├── schemas/ # Pydantic models +│ │ ├── __init__.py-tpl +│ │ └── items.py-tpl +│ └── utils/ # Utility functions +│ ├── __init__.py-tpl +│ └── helpers.py-tpl +├── tests/ # ✅ Tests (required) +│ ├── __init__.py-tpl +│ ├── conftest.py-tpl # pytest configuration +│ └── test_items.py-tpl # Example tests +├── scripts/ # Scripts +│ ├── format.sh-tpl # Code formatting +│ ├── lint.sh-tpl # Linting +│ ├── run-server.sh-tpl # Server execution +│ └── test.sh-tpl # Test execution +├── pyproject.toml-tpl # ✅ Primary metadata (PEP 621, preferred) +├── setup.py-tpl # 🟡 Legacy metadata (accepted for back-compat) +├── requirements.txt-tpl # 🟡 Optional when pyproject declares deps +├── setup.cfg-tpl # Development tools configuration +├── README.md-tpl # ✅ Project documentation (required) +├── .env-tpl # Environment variables template +└── .gitignore-tpl # Git ignore file +``` + +**最少必备文件。** 一个模板至少要提供: + +- `tests/` 目录 +- `README.md-tpl` +- 至少一份元数据文件:`pyproject.toml-tpl`(推荐,PEP 621)或 `setup.py-tpl`(遗留,仍可接受) +- 在以下至少一个位置声明 `fastapi` 依赖:`pyproject.toml-tpl` 的 `[project].dependencies`、`requirements.txt-tpl`,或 `setup.py-tpl` 的 `install_requires` + +当 `pyproject.toml-tpl` 已声明 `[project].dependencies` 时,`requirements.txt-tpl` 不再是严格必需的。新模板**建议**采用 `pyproject.toml-tpl` 作为主要元数据文件。 + +### 文件编写指南 + +#### 1. 编写 main.py-tpl + +```python +""" +FastAPI application entry point + +This file is the main application for the project created with FastAPI-fastkit. +""" +from fastapi import FastAPI +from fastapi.middleware.cors import CORSMiddleware + +from api.api import api_router +from core.config import settings + +# Create FastAPI app (required for inspector validation) +app = FastAPI( + title="", + description="Project created with FastAPI-fastkit", + version="1.0.0", +) + +# CORS middleware configuration +app.add_middleware( + CORSMiddleware, + allow_origins=["*"], + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + +# Register API router +app.include_router(api_router, prefix="/api/v1") + +@app.get("/") +async def root(): + """Root endpoint""" + return {"message": "Hello from !"} + +@app.get("/health") +async def health_check(): + """Health check endpoint""" + return {"status": "healthy"} + +if __name__ == "__main__": + import uvicorn + uvicorn.run(app, host="0.0.0.0", port=8000) +``` + +#### 2. 编写 pyproject.toml-tpl(推荐) + +新模板应使用 PEP 621 的 `pyproject.toml-tpl` 来声明元数据与依赖。该文件至少要有一个 `[project]` 段,内含 `name`、`version`、`description`,以及包含 `fastapi` 的 `dependencies` 列表。模板还必须携带两个 FastAPI-fastkit 身份标识,这样 `is_fastkit_project()` 才能在用户工作区中把生成的项目与无关的 FastAPI 项目区分开: + +- 在 `description` 中带 `[FastAPI-fastkit templated]` 前缀 +- 专门提供 `[tool.fastapi-fastkit]` 表,并设置 `managed = true` + +识别时只需任一标识匹配即可(不区分大小写)。若模板遗漏了它们,元数据注入会在项目生成时自动补上,但作者**应当显式包含**它们。 + +```toml +[project] +name = "" +version = "0.1.0" +description = "[FastAPI-fastkit templated] " +authors = [ + {name = "", email = ""}, +] +readme = "README.md" +requires-python = ">=3.12" +dependencies = [ + "fastapi>=0.115.0", + "uvicorn[standard]>=0.34.0", + "pydantic>=2.10.0", + "pydantic-settings>=2.7.0", + "python-dotenv>=1.0.0", +] + +[project.optional-dependencies] +dev = [ + "pytest>=8.0.0", + "httpx>=0.28.0", +] + +[tool.fastapi-fastkit] +managed = true + +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" +``` + +#### 3. 编写 requirements.txt-tpl(可选) + +当 `pyproject.toml-tpl` 已经声明 `[project].dependencies` 时为可选。对于偏好纯 `pip` 工作流的模板,仍然有用。 + +```txt +# FastAPI core dependencies (required) +fastapi==0.104.1 +uvicorn[standard]==0.24.0 + +# Data validation +pydantic==2.5.0 +pydantic-settings==2.1.0 + +# Environment variable management +python-dotenv==1.0.0 + +# Database (if needed) +sqlalchemy==2.0.23 +alembic==1.13.0 + +# Development tools +pytest==7.4.3 +pytest-asyncio==0.21.1 +httpx==0.25.2 + +# Code quality +black==23.11.0 +isort==5.12.0 +mypy==1.7.1 +``` + +#### 4. 编写 setup.py-tpl(遗留 —— 在已有 pyproject 时为可选) + +保留它是为了兼容遗留模板。如果新模板已附带 `pyproject.toml-tpl`,则无需此文件。 + +```python +""" + package setup + +Project created with FastAPI-fastkit. +""" +from setuptools import find_packages, setup + +# Dependencies list (type annotation required) +install_requires: list[str] = [ + "fastapi>=0.104.0", + "uvicorn[standard]>=0.24.0", + "pydantic>=2.5.0", + "pydantic-settings>=2.1.0", + "python-dotenv>=1.0.0", +] + +setup( + name="", + version="1.0.0", + description="[FastAPI-fastkit templated] ", # Identity marker used by is_fastkit_project() + long_description=open("README.md").read(), + long_description_content_type="text/markdown", + author="", + author_email="", + packages=find_packages(), + install_requires=install_requires, + python_requires=">=3.8", + classifiers=[ + "Development Status :: 4 - Beta", + "Intended Audience :: Developers", + "License :: OSI Approved :: MIT License", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + ], +) +``` + +#### 5. 编写测试文件 + +```python +# tests/test_items.py-tpl +""" +Items API test module +""" +import pytest +from fastapi.testclient import TestClient +from main import app + +client = TestClient(app) + +def test_read_root(): + """Test root endpoint""" + response = client.get("/") + assert response.status_code == 200 + assert "message" in response.json() + +def test_health_check(): + """Test health check""" + response = client.get("/health") + assert response.status_code == 200 + assert response.json() == {"status": "healthy"} + +def test_create_item(): + """Test item creation""" + item_data = { + "name": "Test Item", + "description": "Test Description" + } + response = client.post("/api/v1/items/", json=item_data) + assert response.status_code == 200 + data = response.json() + assert data["name"] == item_data["name"] + assert data["description"] == item_data["description"] + +def test_read_items(): + """Test reading items list""" + response = client.get("/api/v1/items/") + assert response.status_code == 200 + assert isinstance(response.json(), list) +``` + +## 🔍 第 3 步:本地校验 + +### 运行自动校验脚本 + +新模板准备好后,用以下命令进行校验: + +```bash +# Validate all templates +make inspect-templates + +# Validate specific template only +make inspect-template TEMPLATES="fastapi-your-template" + +# Validate with verbose output +python scripts/inspect-templates.py --templates "fastapi-your-template" --verbose +``` + +!!! note + + 您提交 PR 后,**Template PR Inspection** 工作流会自动运行并校验您的模板改动,反馈会直接出现在 PR 上。 + +### 校验清单 + +inspector 会自动校验以下项目: + +#### ✅ 文件结构校验 + +- [ ] 存在 `tests/` 目录 +- [ ] 存在 `README.md-tpl` 文件 +- [ ] 至少存在 `pyproject.toml-tpl`(推荐)或 `setup.py-tpl`(遗留)之一 + +#### ✅ 文件扩展名校验 + +- [ ] 所有 Python 文件使用 `.py-tpl` 扩展名 +- [ ] 不存在 `.py` 扩展名的文件 + +#### ✅ 依赖校验 + +- [ ] `fastapi` 至少声明于以下其一: + - [ ] `pyproject.toml-tpl` 的 `[project].dependencies`(推荐) + - [ ] `requirements.txt-tpl` + - [ ] `setup.py-tpl` 的 `install_requires` + +#### ✅ FastAPI 实现校验 + +- [ ] `main.py-tpl` 中存在 `FastAPI` 的 import +- [ ] `main.py-tpl` 中存在形如 `app = FastAPI()` 的应用创建 + +#### ✅ 测试执行校验 + +- [ ] 成功创建虚拟环境 +- [ ] 成功安装依赖 +- [ ] 所有 pytest 测试通过 + +#### ✅ 自动化模板测试 + +FastAPI-fastkit 自带**自动化模板测试**,会对所有模板执行完整测试: + +**测试覆盖范围:** + +- ✅ 模板创建过程 +- ✅ 项目元数据注入 +- ✅ 虚拟环境搭建 +- ✅ 依赖安装(覆盖所有包管理器) +- ✅ 基本项目结构校验 +- ✅ FastAPI 项目识别 + +**测试执行:** +```console +# Test all templates automatically +$ pytest tests/test_templates/test_all_templates.py -v + +# Test specific template +$ pytest tests/test_templates/test_all_templates.py::TestAllTemplates::test_template_creation[your-template-name] -v +``` + +**模板测试自动发现:** +新模板会被**自动发现**并测试,无需手动配置: + +1. ✅ **零配置**:加入模板即可自动测试 +2. ✅ **一致的测试**:对所有模板使用相同的质量标准 +3. ✅ **多种包管理器**:覆盖 UV、PDM、Poetry 与 PIP +4. ✅ **全面校验**:结构、元数据与功能性都会检查 + +**这对您意味着什么:** + +- 🚀 **`FastAPI-fastkit` 主源码中无需新增测试**:您的模板会被自动测试 +- ⚡ **开发更快**:专注模板内容,而不是测试配置 +- 🛡️ **质量保证**:所有模板拥有一致的测试体验 +- 🔄 **CI/CD 集成**:Pull Request 中自动测试 + +**仍需手动测试的内容:** + +- 🧪 **模板特有的功能**:业务逻辑与定制特性 +- 🔧 **集成测试**:外部服务与复杂工作流 +- 📱 **端到端场景**:完整的用户流程 + +**测试最佳实践:** +```console +# 1. Test your template locally +$ fastkit startdemo your-template-name + +# 2. Run automated tests +$ pytest tests/test_templates/test_all_templates.py::TestAllTemplates::test_template_creation[your-template-name] -v + +# 3. Test with different package managers +$ fastkit startdemo your-template-name --package-manager poetry +$ fastkit startdemo your-template-name --package-manager pdm +$ fastkit startdemo your-template-name --package-manager uv +``` + +### 手动校验清单 + +在自动校验之外,还要手动检查以下项: + +#### 🔧 代码质量 + +- [ ] 代码遵循 PEP 8 风格指南 +- [ ] 合理使用类型注解 +- [ ] 变量与函数命名有意义 +- [ ] 注释与 docstring 得当 + +#### 🏗️ 架构 + +- [ ] 关注点分离(API、业务逻辑、数据访问相互独立) +- [ ] 组件设计可复用 +- [ ] 结构便于扩展 +- [ ] 落实安全最佳实践 + +#### 📚 文档 + +- [ ] README.md-tpl 遵循 PROJECT_README_TEMPLATE.md 的格式 +- [ ] 明确说明安装与运行方式 +- [ ] 提供 API 文档(OpenAPI / Swagger) +- [ ] 解释环境变量 + +## 📚 第 4 步:文档 + +### 编写 README.md-tpl + +参照 [PROJECT_README_TEMPLATE.md](https://github.com/bnbong/FastAPI-fastkit/blob/main/src/fastapi_fastkit/fastapi_project_template/PROJECT_README_TEMPLATE.md) 指南编写。 + +### 编写模板说明文档 + +在 `src/fastapi_fastkit/fastapi_project_template/README.md` 中加入对新模板的说明: + +```markdown +## fastapi-your-template + +Write a brief description and use cases for your new template here. + +### Features: +- Feature 1 +- Feature 2 +- Feature 3 + +### Use Cases: +- Use case 1 +- Use case 2 +``` + +## 🚀 第 5 步:提交与评审 + +### 创建 PR 前的清单 + +- [ ] 全部自动校验通过(`make inspect-templates`) +- [ ] 完成代码格式化(`make format`) +- [ ] 静态检查通过(`make lint`) +- [ ] 所有测试通过(`make test`) +- [ ] 文档已完成 +- [ ] 遵循 CONTRIBUTING.md 的规范 + +### PR 标题与描述 + +``` +[TEMPLATE] Add fastapi-{template-name} template + +## Overview +Adds a new {purpose} template. + +## Key Features +- Feature 1 +- Feature 2 +- Feature 3 + +## Validation Results +- [ ] Inspector validation passed +- [ ] All tests passed +- [ ] Documentation completed + +## Usage Example +\```bash +fastkit startdemo +# Select template: fastapi-{template-name} +\``` + +## Related Issues +Closes #issue-number +``` + +### 评审流程 + +1. **自动校验**:GitHub Actions 自动校验模板 + - **Template PR Inspection**:对修改模板的 PR 运行 `inspect-changed-templates.py` + - **Weekly Inspection**:每周三对所有模板进行完整校验 +2. **代码评审**:维护者与社区评审代码 +3. **测试**:在不同环境中测试模板 +4. **文档评审**:核对文档的准确性与完整性 +5. **批准与合并**:所有要求满足后合并到 main 分支 + +!!! note + + 您会自动收到带有校验结果的 PR 评论。在请求评审前先确认这些结果! + +## 🎯 最佳实践 + +### 安全考量 + +- 用环境变量管理敏感信息 +- 合理配置 CORS +- 校验输入数据 +- 预防 SQL 注入 + +### 性能优化 + +- 善用异步处理 +- 优化数据库查询 +- 合理的缓存策略 +- 配置响应压缩 + +### 可维护性 + +- 代码结构清晰 +- 测试覆盖全面 +- 文档详尽 +- 配置日志与监控 + +## 🆘 需要帮助? + +- 📖 [开发环境配置指南](development-setup.md) +- 📋 [代码规范](code-guidelines.md) +- 💬 [GitHub Discussions](https://github.com/bnbong/FastAPI-fastkit/discussions) +- 📧 [联系维护者](mailto:bbbong9@gmail.com) + +为 FastAPI-fastkit 社区添加新模板是非常宝贵的贡献。 +您的创意与付出会对其他开发者大有帮助!🚀 diff --git a/docs/zh/contributing/translation-guide.md b/docs/zh/contributing/translation-guide.md new file mode 100644 index 0000000..14ad7dd --- /dev/null +++ b/docs/zh/contributing/translation-guide.md @@ -0,0 +1,367 @@ +# 翻译指南 + +本指南说明如何为 FastAPI-fastkit 文档贡献翻译。 + +## 权威来源与翻译政策 + +> **英文(`en`)是 FastAPI-fastkit 文档的权威来源。** 其他所有语言都是翻译目标,可能在某个版本或单个页面上落后于英文。 +> +> 当翻译页面与英文页面不一致时,**请以英文页面为准**,直到翻译跟上为止。翻译以贡献者达到的进度发布 —— 部分覆盖是正常且符合预期的。 + +与该政策相对应的用户视角说明是 [翻译状态](../reference/translation-status.md) 页面,它会列出每个语言的实际完成度,以及尚未翻译时 MkDocs 的呈现方式(简而言之:回退到英文)。 + +仓库根目录的 `CHANGELOG.md` 也保持英文,作为权威发布历史。如果某个语言提供了 `changelog.md` 页面,该页面应当链接或包含规范的英文 changelog,而不要单独维护一份翻译过的 changelog —— 除非将来项目政策做出明确调整。 + +每次贡献翻译时,也请同步更新状态页中的表格,这样用户无需从语言切换器中猜测就能知道有哪些可用内容。 + +## 总览 + +FastAPI-fastkit 使用一套基于 AI 的自动化翻译系统,把文档翻译成多种语言。该系统会: + +- 读取英文源文档 +- 使用 AI API(OpenAI 或 Anthropic)翻译内容 +- 将翻译结果保存到对应语言目录 +- 创建 GitHub Pull Request 以供评审 + +自动化只提供起点,合并前**仍需人工评审**。AI 生成的翻译在 PR 中应标记为「draft」,并由该语言的熟练使用者评审后再合入。 + +## 支持的语言 + +以下是文档站当前**会构建**的语言。仅仅配置了构建目标**并不**意味着该语言已被翻译 —— 真实完成度请参阅 [翻译状态](../reference/translation-status.md)。 + +- 🇰🇷 韩文(ko) +- 🇯🇵 日文(ja) +- 🇨🇳 中文(zh) +- 🇪🇸 西班牙文(es) +- 🇫🇷 法文(fr) +- 🇩🇪 德文(de) + +## 前置条件 + +### 1. 安装翻译相关依赖 + +```bash +# Install using pip +pip install openai anthropic + +# Or using pdm +pdm install -G translation +``` + +### 2. 配置 API key + +您需要 OpenAI 或 Anthropic 任一家的 API key: + +```bash +# For OpenAI +export TRANSLATION_API_KEY="sk-..." + +# Or for Anthropic +export TRANSLATION_API_KEY="sk-ant-..." +``` + +### 3. 安装 GitHub CLI(可选) + +用于自动创建 PR: + +```bash +# macOS +brew install gh + +# Linux +curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg | sudo dd of=/usr/share/keyrings/githubcli-archive-keyring.gpg +echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" | sudo tee /etc/apt/sources.list.d/github-cli.list > /dev/null +sudo apt update +sudo apt install gh + +# Authenticate +gh auth login +``` + +## 使用方式 + +### 使用 Make 命令(推荐) + +运行翻译最便捷的方式: + +```bash +# Translate all docs to all languages +make translate + +# Translate to specific language +make translate LANG=ko + +# Specify API provider and model +make translate LANG=ko PROVIDER=openai MODEL=gpt-4 +make translate LANG=ko PROVIDER=github MODEL=gpt-4o-mini +``` + +### 直接使用脚本 + +#### 翻译全部文档 + +把全部文档翻译为所有支持的语言: + +```bash +python scripts/translate.py --api-provider openai +``` + +### 翻译为指定语言 + +仅翻译为韩文: + +```bash +python scripts/translate.py --target-lang ko --api-provider openai +``` + +### 翻译指定文件 + +只翻译特定的文档文件: + +```bash +python scripts/translate.py \ + --target-lang ko \ + --files user-guide/installation.md user-guide/quick-start.md \ + --api-provider openai +``` + +### 跳过 PR 创建 + +翻译但不创建 GitHub PR: + +```bash +python scripts/translate.py --target-lang ko --no-pr --api-provider openai +``` + +### 使用 Anthropic Claude + +使用 Anthropic 的 Claude 代替 OpenAI: + +```bash +python scripts/translate.py \ + --target-lang ko \ + --api-provider anthropic \ + --api-key "sk-ant-..." +``` + +## 目录结构 + +完成翻译后,文档结构形如: + +``` +docs/ +├── en/ # English (original) +│ ├── index.md +│ ├── user-guide/ +│ │ ├── installation.md +│ │ ├── quick-start.md +│ │ └── ... +│ ├── tutorial/ +│ └── ... +├── ko/ # Korean +│ ├── index.md +│ ├── user-guide/ +│ └── ... +├── ja/ # Japanese +├── zh/ # Chinese +├── es/ # Spanish +├── fr/ # French +├── de/ # German +├── css/ # Shared assets +├── js/ # Shared assets +└── img/ # Shared assets +``` + +## 翻译工作流 + +### 1. 用英文撰写文档 + +所有文档都应先用英文写在 `docs/` 目录: + +```bash +# Create new documentation +vim docs/user-guide/new-feature.md +``` + +### 2. 运行翻译 + +英文文档完成后,运行翻译脚本: + +```bash +python scripts/translate.py --target-lang ko +``` + +### 3. 评审 Pull Request + +脚本会创建包含翻译结果的 Pull Request。评审时请: + +1. 检查 markdown 格式是否保留 +2. 核对技术术语是否处理得当 +3. 确认代码示例未被改动 +4. 检查与目标语言相关的具体问题 + +### Changelog 政策 + +- 仓库根目录的 `CHANGELOG.md` 保持英文。 +- 不要开启「把根 changelog 中的发布历史改写成其他语言」的翻译 PR。 +- 如果某个语言需要 changelog 页面,请把 `docs//changelog.md` 视为规范英文 changelog 的包装页或入口页。 + +### 4. 批准并合并(面向维护者) + +翻译核对完毕后: + +```bash +gh pr review --approve +gh pr merge +``` + +### 5. 部署文档 + +文档站会自动重建,新增的翻译会随之上线。 + +## 翻译配置 + +编辑 `scripts/translation_config.json` 进行定制: + +```json +{ + "source_language": "en", + "target_languages": [ + { + "code": "ko", + "name": "Korean", + "native_name": "한국어", + "enabled": true + } + ], + "translation_settings": { + "default_api_provider": "openai", + "batch_size": 5, + "preserve_formatting": true + }, + "github_settings": { + "create_pr_by_default": true, + "branch_prefix": "translation" + } +} +``` + +## 最佳实践 + +### 源文档相关 + +1. **使用清晰的语言**:让英文表达清楚、简洁,便于翻译 +2. **术语一致**:对技术术语保持一致 +3. **正确使用代码块**:始终在代码块上标明语言 +4. **链接核对**:内部链接都使用相对路径 + +### 翻译评审相关 + +1. **技术术语**:确认这些术语在目标语言下是否恰当 +2. **文化语境**:判断示例是否需要本地化 +3. **格式**:保留全部 markdown 格式 +4. **代码完整性**:确认代码块未被改动 + +## 故障排查 + +### 触发 API 限流 + +如果触发 API 限流,可按更小的批次翻译: + +```bash +# Translate only user guide +python scripts/translate.py \ + --target-lang ko \ + --files user-guide/*.md +``` + +### 翻译质量问题 + +若翻译质量不佳: + +1. 检查 API key 是否有效 +2. 换一家 AI provider 试试 +3. 把复杂文档拆成更小的章节 +4. 手动评审并修订翻译 + +### GitHub PR 创建失败 + +如果 PR 创建失败: + +```bash +# Translate without PR +python scripts/translate.py --target-lang ko --no-pr + +# Manually create PR +git checkout -b translation/ko +git add docs/ko/ +git commit -m "Add Korean translations" +git push -u origin translation/ko +gh pr create --title "Add Korean translations" +``` + +## 手动翻译 + +也可以完全手动翻译: + +1. 把英文文件复制到目标语言目录: +```bash +mkdir -p docs/ko/user-guide +cp docs/en/user-guide/installation.md docs/ko/user-guide/installation.md +``` + +2. 用您喜欢的编辑器编辑该文件 +3. 提交并创建 PR + +## 语言切换 + +文档站顶部导航中包含语言切换器,用户可以: + +1. 点击语言选择器 +2. 选择心仪的语言 +3. 在翻译过的文档间浏览 + +## 贡献新语言 + +要新增一种语言: + +1. 编辑 `scripts/translation_config.json`: +```json +{ + "code": "pt", + "name": "Portuguese", + "native_name": "Português", + "enabled": true +} +``` + +2. 更新 `mkdocs.yml`: +```yaml +- locale: pt + name: Português + build: true +``` + +3. 运行翻译: +```bash +python scripts/translate.py --target-lang pt +``` + +## 需要帮助? + +- **Issues**:在 [GitHub Issues](https://github.com/bnbong/FastAPI-fastkit/issues) 反馈翻译问题 +- **Discussions**:在 [GitHub Discussions](https://github.com/bnbong/FastAPI-fastkit/discussions) 提问 +- **Contributing**:参见 [CONTRIBUTING.md](https://github.com/bnbong/FastAPI-fastkit/blob/main/CONTRIBUTING.md) + +## 翻译质量标准 + +所有翻译都必须达到以下标准: + +- ✅ 保留全部 markdown 格式 +- ✅ 保持代码块原样不动 +- ✅ 妥善处理技术术语 +- ✅ 语法与拼写正确 +- ✅ 遵循目标语言的约定 +- ✅ 验证所有链接可用 + +感谢您为 FastAPI-fastkit 的翻译工作贡献力量!🌍 diff --git a/docs/zh/index.md b/docs/zh/index.md new file mode 100644 index 0000000..4df9647 --- /dev/null +++ b/docs/zh/index.md @@ -0,0 +1,577 @@ +

+ FastAPI-fastkit +

+

+FastAPI-fastkit:面向 Python 与 FastAPI 新手的快速、易用启动套件 +

+

+ + PyPI - Version + + + GitHub Release + + + PyPI Downloads + +

+ +--- + +本项目旨在帮助 Python 与 [FastAPI](https://github.com/fastapi/fastapi) 新手更快完成基于 Python 的 Web 应用开发环境配置。 + +本项目的灵感来自 `Spring Boot Initializr` 以及 Django 的 `django-admin` CLI 工具。 + +!!! info "翻译状态" + 英文是本文档的权威来源。语言切换器中的其他语言可能尚未完整翻译,缺失页面会按页回退到英文。请参阅 [翻译状态](reference/translation-status.md) 了解各语言版本的实际覆盖情况。 + +## 主要特性 + +- **⚡ 即时创建 FastAPI 项目**:通过 CLI 快速创建 FastAPI 工作区与项目,灵感来自 [Django](https://github.com/django/django) 的 `django-admin` +- **✨ 交互式项目构建器**:对数据库、认证、缓存、监控等进行分步引导式选择,并自动生成代码 +- **🎨 更美观的 CLI 输出**:基于 [rich](https://github.com/Textualize/rich) 提供更好的 CLI 体验 +- **📋 基于标准的 FastAPI 项目模板**:所有 FastAPI-fastkit 模板都遵循 Python 标准与 FastAPI 常见实践 +- **🔍 自动化的模板质量保障**:通过每周自动化测试,确保所有模板始终可用并保持更新 +- **🚀 多种项目模板**:提供多种预配置模板,覆盖 async CRUD、Docker、PostgreSQL 等不同使用场景 +- **📦 支持多种包管理器**:可选用您偏好的 Python 包管理器(pip、uv、pdm、poetry)管理依赖 + +## 安装 + +请在您的 Python 环境中安装 `FastAPI-fastkit`。 + +
+ +```console +$ pip install FastAPI-fastkit +---> 100% +``` + +
+ + +## 使用方法 + +### 立即创建新的 FastAPI 项目工作区 + +现在您可以使用 FastAPI-fastkit 快速启动一个新的 FastAPI 项目。 + +使用下面的命令即可创建新的 FastAPI 项目工作区: + +
+ +```console +$ fastkit init +Enter the project name: my-awesome-project +Enter the author name: John Doe +Enter the author email: john@example.com +Enter the project description: My awesome FastAPI project + + Project Information +┌──────────────┬────────────────────────────┐ +│ Project Name │ my-awesome-project │ +│ Author │ John Doe │ +│ Author Email │ john@example.com │ +│ Description │ My awesome FastAPI project │ +└──────────────┴────────────────────────────┘ + +Available Stacks and Dependencies: + MINIMAL Stack +┌──────────────┬───────────────────┐ +│ Dependency 1 │ fastapi │ +│ Dependency 2 │ uvicorn │ +│ Dependency 3 │ pydantic │ +│ Dependency 4 │ pydantic-settings │ +└──────────────┴───────────────────┘ + + STANDARD Stack +┌──────────────┬───────────────────┐ +│ Dependency 1 │ fastapi │ +│ Dependency 2 │ uvicorn │ +│ Dependency 3 │ sqlalchemy │ +│ Dependency 4 │ alembic │ +│ Dependency 5 │ pytest │ +│ Dependency 6 │ pydantic │ +│ Dependency 7 │ pydantic-settings │ +└──────────────┴───────────────────┘ + + FULL Stack +┌──────────────┬───────────────────┐ +│ Dependency 1 │ fastapi │ +│ Dependency 2 │ uvicorn │ +│ Dependency 3 │ sqlalchemy │ +│ Dependency 4 │ alembic │ +│ Dependency 5 │ pytest │ +│ Dependency 6 │ redis │ +│ Dependency 7 │ celery │ +│ Dependency 8 │ pydantic │ +│ Dependency 9 │ pydantic-settings │ +└──────────────┴───────────────────┘ + +Select stack (minimal, standard, full): minimal + +Available Package Managers: + Package Managers +┌────────┬────────────────────────────────────────────┐ +│ PIP │ Standard Python package manager │ +│ UV │ Fast Python package manager │ +│ PDM │ Modern Python dependency management │ +│ POETRY │ Python dependency management and packaging │ +└────────┴────────────────────────────────────────────┘ + +Select package manager (pip, uv, pdm, poetry) [uv]: uv +Do you want to proceed with project creation? [y/N]: y +FastAPI project will deploy at '~your-project-path~' + +╭──────────────────────── Info ────────────────────────╮ +│ ℹ Injected metadata into setup.py │ +╰──────────────────────────────────────────────────────╯ +╭──────────────────────── Info ────────────────────────╮ +│ ℹ Injected metadata into config file │ +╰──────────────────────────────────────────────────────╯ + + Creating Project: + my-awesome-project +┌───────────────────┬───────────┐ +│ Component │ Collected │ +│ fastapi │ ✓ │ +│ uvicorn │ ✓ │ +│ pydantic │ ✓ │ +│ pydantic-settings │ ✓ │ +└───────────────────┴───────────┘ + +Creating virtual environment... + +╭──────────────────────── Info ────────────────────────╮ +│ ℹ venv created at │ +│ ~your-project-path~/my-awesome-project/.venv │ +│ To activate the virtual environment, run: │ +│ │ +│ source │ +│ ~your-project-path~/my-awesome-project/.venv/bin/act │ +│ ivate │ +╰──────────────────────────────────────────────────────╯ + +Installing dependencies... +⠙ Setting up project environment...Collecting + +---> 100% + +╭─────────────────────── Success ───────────────────────╮ +│ ✨ Dependencies installed successfully │ +╰───────────────────────────────────────────────────────╯ +╭─────────────────────── Success ───────────────────────╮ +│ ✨ FastAPI project 'my-awesome-project' has been │ +│ created successfully and saved to │ +│ ~your-project-path~! │ +╰───────────────────────────────────────────────────────╯ +╭──────────────────────── Info ────────────────────────╮ +│ ℹ To start your project, run 'fastkit runserver' at │ +│ newly created FastAPI project directory │ +╰──────────────────────────────────────────────────────╯ +``` + +
+ +该命令将创建一个包含 Python 虚拟环境的新 FastAPI 项目工作区。 + +### 使用交互模式创建项目 ✨ 新功能 + +如果项目需求更复杂,可以使用 **交互模式** 通过功能选择一步步搭建 FastAPI 应用: + +
+ +```console +$ fastkit init --interactive + +⚡ FastAPI-fastkit Interactive Project Setup ⚡ + +📋 Basic Project Information +Enter the project name: my-fullstack-project +Enter the author name: John Doe +Enter the author email: john@example.com +Enter the project description: Full-stack FastAPI project with PostgreSQL and JWT + +🧱 Architecture Preset +Pick a project layout. Press Enter to accept the recommended default. + 1. minimal - Smallest viable FastAPI app + 2. single-module - Everything in one module (prototypes / scripts) + 3. classic-layered - api/routes + crud + schemas + core (à la fastapi-default) + 4. domain-starter - Domain-oriented src/app/domains// (recommended) + +Select architecture preset: [4] + +🗄️ Database Selection +Select database (PostgreSQL, MySQL, MongoDB, Redis, SQLite, None): + 1. PostgreSQL - PostgreSQL database with SQLAlchemy + 2. MySQL - MySQL database with SQLAlchemy + 3. MongoDB - MongoDB with motor async driver + 4. Redis - Redis for caching and session storage + 5. SQLite - SQLite database for development + 6. None - No database + +Select database: 1 + +🔐 Authentication Selection +Select authentication (JWT, OAuth2, FastAPI-Users, Session-based, None): + 1. JWT - JSON Web Token authentication + 2. OAuth2 - OAuth2 with password flow + 3. FastAPI-Users - Full featured user management + 4. Session-based - Cookie-based sessions + 5. None - No authentication + +Select authentication: 1 + +⚙️ Background Tasks Selection +Select background tasks (Celery, Dramatiq, None): + 1. Celery - Distributed task queue + 2. Dramatiq - Fast and reliable task processing + 3. None - No background tasks + +Select background tasks: 1 + +💾 Caching Selection +Select caching (Redis, fastapi-cache2, None): + 1. Redis - Redis caching + 2. fastapi-cache2 - Simple caching for FastAPI + 3. None - No caching + +Select caching: 1 + +📊 Monitoring Selection +Select monitoring (Loguru, OpenTelemetry, Prometheus, None): + 1. Loguru - Simple and powerful logging + 2. OpenTelemetry - Observability framework + 3. Prometheus - Metrics and monitoring + 4. None - No monitoring + +Select monitoring: 3 + +🧪 Testing Framework Selection +Select testing framework (Basic, Coverage, Advanced, None): + 1. Basic - pytest + httpx for API testing + 2. Coverage - Basic + code coverage + 3. Advanced - Coverage + faker + factory-boy for fixtures + 4. None - No testing framework + +Select testing framework: 2 + +🛠️ Additional Utilities +Select utilities (comma-separated numbers, e.g., 1,3,4): + 1. CORS - Cross-Origin Resource Sharing + 2. Rate-Limiting - Request rate limiting + 3. Pagination - Pagination support + 4. WebSocket - WebSocket support + +Select utilities: 1 + +🚀 Deployment Configuration +Select deployment option: + 1. Docker - Generate Dockerfile + 2. docker-compose - Generate docker-compose.yml (includes Docker) + 3. None - No deployment configuration + +Select deployment option: 2 + +📦 Package Manager Selection +Select package manager (pip, uv, pdm, poetry): uv + +📝 Custom Packages (optional) +Enter custom package names (comma-separated, press Enter to skip): + +📋 Project Configuration Summary +┌─────────────────────┬───────────────────────────────────────────────────────────────────────────┐ +│ Setting │ Value │ +├─────────────────────┼───────────────────────────────────────────────────────────────────────────┤ +│ Project Name │ my-fullstack-project │ +│ Author │ John Doe │ +│ Email │ john@example.com │ +│ Description │ Full-stack FastAPI project with PostgreSQL and JWT │ +│ Architecture Preset │ domain-starter — Domain-oriented: src/app/domains// (recommended)│ +│ Database │ PostgreSQL │ +│ Authentication │ JWT │ +│ Async Tasks │ Celery │ +│ Caching │ Redis │ +│ Monitoring │ Prometheus │ +│ Testing │ Coverage │ +│ Utilities │ CORS │ +│ Package Manager │ uv │ +└─────────────────────┴───────────────────────────────────────────────────────────────────────────┘ + +Total dependencies to install: 18 + +Proceed with project creation? [Y/n]: y + +╭──────────────────────── Info ────────────────────────╮ +│ ℹ Injected metadata into pyproject.toml │ +╰──────────────────────────────────────────────────────╯ +╭─────────────────────── Success ───────────────────────╮ +│ ✨ Generated dependency file with 18 packages │ +╰───────────────────────────────────────────────────────╯ +╭──────────────────────── Info ────────────────────────╮ +│ ℹ Preserving template-shipped main.py for preset │ +│ 'domain-starter'. │ +╰──────────────────────────────────────────────────────╯ +╭─────────────────────── Success ───────────────────────╮ +│ ✨ Generated Docker deployment files │ +╰───────────────────────────────────────────────────────╯ +╭────────────────────── Warning ────────────────────────╮ +│ ⚠ Preset compatibility │ +│ fastapi-domain-starter's shipped src/app/main.py is │ +│ preserved. The selections below need manual wiring │ +│ there (CORS is already wired — set │ +│ BACKEND_CORS_ORIGINS in .env to activate it). │ +│ Affected selections (packages installed, but no │ +│ dynamic main.py edits applied for the │ +│ 'domain-starter' preset): Prometheus │ +╰───────────────────────────────────────────────────────╯ +╭─────────────────────── Success ───────────────────────╮ +│ ✨ Generated configuration files for selected stack │ +╰───────────────────────────────────────────────────────╯ + +Creating virtual environment... +Installing dependencies... + +----> 100% + +╭─────────────────────── Success ───────────────────────╮ +│ ✨ FastAPI project 'my-fullstack-project' from │ +│ 'fastapi-domain-starter' has been created! │ +╰───────────────────────────────────────────────────────╯ +``` + +
+ +交互模式提供: + +- **架构预设选择**(`minimal` / `single-module` / `classic-layered` / `domain-starter`),用于决定基础模板与项目布局 +- **引导式功能选择**,覆盖数据库、认证、后台任务、缓存、监控等配置 +- **自动生成代码** —— 会根据预设行为有所不同(`minimal` / `single-module` 会重新生成 `main.py`;`classic-layered` / `domain-starter` 会保留模板自带的 `main.py` 并补充配置模块) +- **感知预设的 Docker 生成** —— 生成的 `Dockerfile` 中 `CMD` 会指向当前预设真正的入口点(`src.main:app` 或 `src.app.main:app`) +- **智能依赖管理**,自动兼容 pip +- **功能校验**,当预设无法自动完成接入时会给出手动处理提示 +- **项目身份标识** 会写入生成的 `pyproject.toml`(description 标识 + `[tool.fastapi-fastkit]` 节),便于后续由 `is_fastkit_project()` 识别生成的项目 + +### 向 FastAPI 项目添加新路由 + +`FastAPI-fastkit` 让扩展 FastAPI 项目变得更简单。 + +使用以下命令向您的 FastAPI 项目添加一个新的路由端点: + +
+ +```console +$ fastkit addroute user my-awesome-project + Adding New Route +┌──────────────────┬──────────────────────────────────────────┐ +│ Project │ my-awesome-project │ +│ Route Name │ user │ +│ Target Directory │ ~your-project-path~ │ +└──────────────────┴──────────────────────────────────────────┘ + +Do you want to add route 'user' to project 'my-awesome-project'? [Y/n]: y + +╭──────────────────────── Info ────────────────────────╮ +│ ℹ Updated main.py to include the API router │ +╰──────────────────────────────────────────────────────╯ +╭─────────────────────── Success ───────────────────────╮ +│ ✨ Successfully added new route 'user' to project │ +│ `my-awesome-project` │ +╰───────────────────────────────────────────────────────╯ +``` + +
+ +### 立即部署一个结构化的 FastAPI 示例项目 + +您也可以从一个结构完整的 FastAPI 示例项目开始。 + +这些示例项目覆盖不同技术栈,并内置了简单的 item CRUD 端点实现。 + +使用下面的命令即可创建一个结构完整的 FastAPI 示例项目: + +
+ +```console +$ fastkit startdemo +Enter the project name: my-awesome-demo +Enter the author name: John Doe +Enter the author email: john@example.com +Enter the project description: My awesome FastAPI demo +Deploying FastAPI project using 'fastapi-default' template +Template path: +/~fastapi_fastkit-package-path~/fastapi_project_template/fastapi-default + + Project Information +┌──────────────┬─────────────────────────┐ +│ Project Name │ my-awesome-demo │ +│ Author │ John Doe │ +│ Author Email │ john@example.com │ +│ Description │ My awesome FastAPI demo │ +└──────────────┴─────────────────────────┘ + + Template Dependencies +┌──────────────┬───────────────────┐ +│ Dependency 1 │ fastapi │ +│ Dependency 2 │ uvicorn │ +│ Dependency 3 │ pydantic │ +│ Dependency 4 │ pydantic-settings │ +│ Dependency 5 │ python-dotenv │ +└──────────────┴───────────────────┘ + +Available Package Managers: + Package Managers +┌────────┬────────────────────────────────────────────┐ +│ PIP │ Standard Python package manager │ +│ UV │ Fast Python package manager │ +│ PDM │ Modern Python dependency management │ +│ POETRY │ Python dependency management and packaging │ +└────────┴────────────────────────────────────────────┘ + +Select package manager (pip, uv, pdm, poetry) [uv]: uv +Do you want to proceed with project creation? [y/N]: y +FastAPI template project will deploy at '~your-project-path~' + +---> 100% + +╭─────────────────────── Success ───────────────────────╮ +│ ✨ Dependencies installed successfully │ +╰───────────────────────────────────────────────────────╯ +╭─────────────────────── Success ───────────────────────╮ +│ ✨ FastAPI project 'my-awesome-demo' from │ +│ 'fastapi-default' has been created and saved to │ +│ ~your-project-path~! │ +╰───────────────────────────────────────────────────────╯ +``` + +
+ +如需查看可用的 FastAPI 示例列表,请运行: + +
+ +```console +$ fastkit list-templates + Available Templates +┌────────────────────────┬───────────────────────────────────────────────────────┐ +│ fastapi-custom-response│ Async Item Management API with Custom Response System │ +│ fastapi-mcp │ FastAPI MCP Project │ +│ fastapi-domain-starter │ FastAPI Domain Starter │ +│ fastapi-dockerized │ Dockerized FastAPI Item Management API │ +│ fastapi-empty │ Minimal FastAPI Template │ +│ fastapi-async-crud │ Async Item Management API Server │ +│ fastapi-psql-orm │ Dockerized FastAPI Item Management API with │ +│ │ PostgreSQL │ +│ fastapi-default │ Simple FastAPI Project │ +│ fastapi-single-module │ FastAPI Single Module Template │ +└────────────────────────┴───────────────────────────────────────────────────────┘ +``` + +
+ +## 文档 + +如需更完整的指南与详细使用说明,请查阅我们的文档: + +- 📚 **[用户指南](user-guide/quick-start.md)** —— 详细的安装与使用指南 +- 🎯 **[教程](tutorial/getting-started.md)** —— 面向新手的分步教程 +- 📖 **[CLI 参考](user-guide/cli-reference.md)** —— 完整的命令参考 +- 🔍 **[模板质量保障](reference/template-quality-assurance.md)** —— 自动化测试与质量标准 + +## 🚀 基于模板的教程 + +通过我们的预构建模板,在实际用例中学习 FastAPI 开发: + +### 📖 核心教程 + +- **[构建基础 API 服务器](tutorial/basic-api-server.md)** —— 使用 `fastapi-default` 模板创建您的第一个 FastAPI 服务器 +- **[构建异步 CRUD API](tutorial/async-crud-api.md)** —— 使用 `fastapi-async-crud` 模板开发高性能异步 API +- **[领域驱动项目(Domain Starter)](tutorial/domain-starter.md)** —— 使用 `fastapi-domain-starter` 模板构建中型规模的 API,这是推荐的现代默认选项 + +### 🗄️ 数据库与基础设施 + +- **[集成数据库](tutorial/database-integration.md)** —— 借助 `fastapi-psql-orm` 模板使用 PostgreSQL + SQLAlchemy +- **[Docker 化与部署](tutorial/docker-deployment.md)** —— 借助 `fastapi-dockerized` 模板搭建生产部署环境 + +### ⚡ 高级功能 + +- **[自定义响应处理与高级 API 设计](tutorial/custom-response-handling.md)** —— 借助 `fastapi-custom-response` 模板构建企业级 API +- **[集成 MCP](tutorial/mcp-integration.md)** —— 借助 `fastapi-mcp` 模板创建与 AI 模型集成的 API 服务器 + +每个教程包含: + +- ✅ **实用示例** —— 可直接用于真实项目的代码 +- ✅ **分步指南** —— 详细的说明,易于新手跟随 +- ✅ **最佳实践** —— 行业标准模式与安全考量 +- ✅ **拓展方法** —— 帮助您将项目推向新阶段 + +## 贡献 + +我们欢迎社区的贡献!FastAPI-fastkit 旨在帮助 Python 与 FastAPI 的新手,而您的贡献能带来巨大的影响。 + +### 您可以贡献什么 + +- 🚀 **新的 FastAPI 模板** —— 为不同的场景添加模板 +- 🐛 **修复缺陷** —— 帮助我们提升稳定性与可靠性 +- 📚 **文档** —— 改进指南、示例与翻译 +- 🧪 **测试** —— 提高测试覆盖率,补充集成测试 +- 💡 **功能** —— 提议并实现新的 CLI 功能 + +### 如何开始贡献 + +要开始为 FastAPI-fastkit 贡献,请参考我们的完整指南: + +- **[开发环境设置](contributing/development-setup.md)** —— 配置开发环境的完整指南 +- **[代码规范](contributing/code-guidelines.md)** —— 编码标准与最佳实践 +- **[CONTRIBUTING.md](https://github.com/bnbong/FastAPI-fastkit/blob/main/CONTRIBUTING.md)** —— 完整的贡献指南 +- **[CODE_OF_CONDUCT.md](https://github.com/bnbong/FastAPI-fastkit/blob/main/CODE_OF_CONDUCT.md)** —— 项目原则与社区准则 +- **[SECURITY.md](https://github.com/bnbong/FastAPI-fastkit/blob/main/SECURITY.md)** —— 安全指引与上报方式 + +## FastAPI-fastkit 的意义 + +FastAPI-fastkit 的目标是为 Python 与 FastAPI 的新手提供一个快速、易用的启动套件。 + +这个想法的初衷,是帮助 FastAPI 新手从一开始就轻松上手 —— 这与 [FastAPI 0.111.0 版本更新](https://github.com/fastapi/fastapi/releases/tag/0.111.0) 中加入的 FastAPI-cli 包对生产环境的意义不谋而合。 + +作为一名长期使用并热爱 FastAPI 的开发者,我希望开发一个项目,可以帮助实现 FastAPI 开发者 [tiangolo](https://github.com/tiangolo) 所表达的 [那份美好初衷](https://github.com/fastapi/fastapi/pull/11522#issuecomment-2264639417)。 + +FastAPI-fastkit 通过以下几点,在「快速上手」与「构建生产级应用」之间架起桥梁: + +- **新手即可立刻获得生产力**,不再被复杂的配置流程吓退 +- **每个模板内置最佳实践**,帮助使用者学习正确的 FastAPI 模式 +- **可扩展的基础**,陪伴使用者从新手成长为专家 +- **社区驱动的模板**,反映真实世界的 FastAPI 使用模式 + +## 下一步 + +准备好开始使用 FastAPI-fastkit 了吗?请按以下步骤继续: + +### 🚀 快速开始 + +1. **[安装](user-guide/installation.md)**:安装 FastAPI-fastkit +2. **[快速上手](user-guide/quick-start.md)**:5 分钟内创建您的第一个项目 +3. **[入门教程](tutorial/getting-started.md)**:分步详细教程 + +### 📚 进阶学习 + +- **[创建项目](user-guide/creating-projects.md)**:使用不同的技术栈创建项目 +- **[添加路由](user-guide/adding-routes.md)**:为您的项目添加 API 端点 +- **[使用模板](user-guide/using-templates.md)**:使用预构建的项目模板 + +### 🛠️ 参与贡献 + +想要为 FastAPI-fastkit 做贡献? + +- **[开发环境设置](contributing/development-setup.md)**:配置您的开发环境 +- **[代码规范](contributing/code-guidelines.md)**:遵循我们的编码标准与最佳实践 +- **[贡献指南](https://github.com/bnbong/FastAPI-fastkit/blob/main/CONTRIBUTING.md)**:完整的贡献指南 + +### 🔍 参考 + +- **[CLI 参考](user-guide/cli-reference.md)**:完整的 CLI 命令参考 +- **[模板质量保障](reference/template-quality-assurance.md)**:自动化测试与质量标准 +- **[FAQ](reference/faq.md)**:常见问题解答 +- **[GitHub 仓库](https://github.com/bnbong/FastAPI-fastkit)**:源代码与问题跟踪 + +## 许可证 + +本项目采用 MIT 许可证 —— 详细信息见 [LICENSE](https://github.com/bnbong/FastAPI-fastkit/blob/main/LICENSE) 文件。 diff --git a/docs/zh/reference/faq.md b/docs/zh/reference/faq.md new file mode 100644 index 0000000..c9c9f21 --- /dev/null +++ b/docs/zh/reference/faq.md @@ -0,0 +1,784 @@ +# 常见问题 + +围绕 FastAPI-fastkit 的常见问答。 + +## 安装与配置 + +### 问:支持哪些 Python 版本? + +**答:** FastAPI-fastkit 需要 **Python 3.12 及以上**。我们建议使用最新的稳定版 Python,以获得最佳体验。 + +
+ +```console +$ python --version +Python 3.12.1 + +$ pip install fastapi-fastkit +``` + +
+ +### 问:如何安装 FastAPI-fastkit? + +**答:** 使用 pip 即可安装: + +
+ +```console +# Latest stable version +$ pip install fastapi-fastkit + +# Development version from GitHub +$ pip install git+https://github.com/bnbong/FastAPI-fastkit.git + +# Specific version +$ pip install fastapi-fastkit==1.0.0 +``` + +
+ +### 问:安装时出现权限错误失败 + +**答:** 请尝试在虚拟环境中安装,或以当前用户身份安装: + +
+ +```console +# Create virtual environment +$ python -m venv fastapi-env +$ source fastapi-env/bin/activate # On Windows: fastapi-env\Scripts\activate + +# Install in virtual environment +$ pip install fastapi-fastkit + +# Or install for current user only +$ pip install --user fastapi-fastkit +``` + +
+ +### 问:安装后找不到 `fastkit` 命令 + +**答:** 这通常意味着安装目录不在您的 PATH 中: + +
+ +```console +# Check if installed +$ pip show fastapi-fastkit + +# Find installation location +$ python -c "import fastapi_fastkit; print(fastapi_fastkit.__file__)" + +# Try running directly +$ python -m fastapi_fastkit --version + +# Or add to PATH (Linux/macOS) +$ export PATH="$HOME/.local/bin:$PATH" +``` + +
+ +## 创建项目 + +### 问:可选哪些依赖栈? + +**答:** FastAPI-fastkit 提供三种依赖栈: + +- **MINIMAL**:FastAPI、Uvicorn、Pydantic、Pydantic-Settings(基础 Web API) +- **STANDARD**:再加上 SQLAlchemy、Alembic、pytest(数据库支持) +- **FULL**:再加上 Redis、Celery(后台任务) + +!!! tip "默认包管理器" + 默认包管理器是 `uv`,依赖安装更快。您也可以选择 `pip`、`pdm` 或 `poetry`。 + +
+ +```console +$ fastkit init +# Select your preferred stack during project creation +``` + +
+ +### 问:可以自定义项目模板吗? + +**答:** 可以!您可以: + +1. **使用现有模板**(`fastkit startdemo`) +2. **创建自定义模板** —— 在现有模板基础上复制并修改 +3. **逐步添加路由**(`fastkit addroute`) + +
+ +```console +# Use pre-built templates +$ fastkit list-templates +$ fastkit startdemo + +# Add routes to existing project +$ fastkit addroute users . # Add 'users' route to current directory +$ fastkit addroute users my-project # Add 'users' route to 'my-project' +``` + +
+ +### 问:项目名称有什么格式要求? + +**答:** 项目名称必须是合法的 Python 标识符: + +- ✅ `my-api`、`blog_system`、`UserService` +- ❌ `my api`、`123project`、`project-name!` + +
+ +```console +$ fastkit init +Enter the project name: my_awesome_api # Valid +Enter the project name: my-awesome-api # Valid (hyphens converted to underscores) +``` + +
+ +### 问:创建项目失败,提示「directory already exists」 + +**答:** 项目目录已经存在。处理方式: + +1. **换一个名字** +2. **删除已有目录**(确认安全后) +3. **使用其他输出位置** + +
+ +```console +# Check if directory exists +$ ls my-project + +# Remove if safe (CAUTION!) +$ rm -rf my-project + +# Or create in different location +$ mkdir projects +$ cd projects +$ fastkit init +``` + +
+ +### 问:如何用交互模式创建项目? + +**答:** 使用 `fastkit init --interactive` 进入引导式逐步项目创建,并自动智能选择功能: + +
+ +```console +$ fastkit init --interactive +``` + +
+ +交互模式会依次带您完成以下步骤: + +1. **项目信息** —— 名称、作者、邮箱、描述。 +2. **架构预设** —— 选定项目布局。推荐默认是 `domain-starter`;直接回车即可接受。每种预设的具体布局,以及哪些功能组合需要手动接入,详见 [架构预设矩阵](preset-feature-matrix.md)。 +3. **功能选择** —— 数据库、认证、后台任务、缓存、监控、测试、工具、部署。 +4. **包管理器与自定义包** —— pip / uv / pdm / poetry,以及您想固定下来的额外依赖。 +5. **确认** —— 在项目创建之前展示一张总结表,列出全部选择(包含架构预设)。 + +交互模式提供丰富的功能目录可选: + +| 类别 | 可选项 | +|----------|-------------------| +| **架构** | minimal、single-module、classic-layered、**domain-starter**(推荐默认) | +| **数据库** | PostgreSQL、MySQL、MongoDB、Redis、SQLite | +| **认证** | JWT、OAuth2、FastAPI-Users、基于会话 | +| **后台任务** | Celery、Dramatiq | +| **测试** | Basic(pytest)、Coverage、Advanced(附带 faker、factory-boy) | +| **缓存** | 基于 fastapi-cache2 的 Redis | +| **监控** | Loguru、OpenTelemetry、Prometheus | +| **工具** | CORS、限流、分页、WebSocket | +| **部署** | Docker、docker-compose,自动生成配置 | + +交互模式会自动生成: + +- 一个集成所选功能的 `main.py` +- 当所选选项支持代码生成时,会生成数据库与认证的配置文件(例如数据库选 PostgreSQL/MySQL/SQLite/MongoDB,认证选 JWT/FastAPI-Users);其他选项仅安装必要的包 +- 与所选部署选项匹配的部署文件(选了 `Docker` 会生成 `Dockerfile`,选了 `docker-compose` 会生成 `docker-compose.yml`) +- 基于所选测试选项的测试配置(只有选择 `Coverage` 或 `Advanced` 时才会包含覆盖率设置) + +### 问:如何查看交互模式可用的功能? + +**答:** 使用 `list-features` 命令,可以展示所有可用功能及其包: + +
+ +```console +$ fastkit list-features +# Shows all available features organized by category +# with their associated packages +``` + +
+ +这可以帮助您理解每个功能选择会安装哪些包。 + +## 路由开发 + +### 问:如何为路由加上认证? + +**答:** 创建一个用于认证的依赖: + +```python +# src/api/deps.py +from fastapi import Depends, HTTPException, status +from fastapi.security import HTTPBearer + +security = HTTPBearer() + +def get_current_user(token: str = Depends(security)): + # Verify token and return user + if not verify_token(token): + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Invalid authentication credentials" + ) + return get_user_from_token(token) + +# src/api/routes/users.py +@router.get("/me") +def get_current_user_profile(user = Depends(get_current_user)): + return user +``` + +### 问:如何为项目添加数据库模型? + +**答:** 在 STANDARD 或 FULL 栈中,创建 SQLAlchemy 模型: + +```python +# src/models/users.py +from sqlalchemy import Column, Integer, String, Boolean +from sqlalchemy.ext.declarative import declarative_base + +Base = declarative_base() + +class User(Base): + __tablename__ = "users" + + id = Column(Integer, primary_key=True, index=True) + email = Column(String, unique=True, index=True) + username = Column(String, unique=True, index=True) + hashed_password = Column(String) + is_active = Column(Boolean, default=True) +``` + +### 问:如何为请求数据加校验? + +**答:** 在 schema 中使用 Pydantic 模型: + +```python +# src/schemas/users.py +from pydantic import BaseModel, EmailStr, Field + +class UserCreate(BaseModel): + email: EmailStr + username: str = Field(..., min_length=3, max_length=50) + password: str = Field(..., min_length=8) + + @validator('username') + def validate_username(cls, v): + if not v.isalnum(): + raise ValueError('Username must be alphanumeric') + return v +``` + +### 问:如何处理文件上传? + +**答:** 使用 FastAPI 的 `UploadFile`: + +```python +from fastapi import UploadFile, File + +@router.post("/upload") +async def upload_file(file: UploadFile = File(...)): + contents = await file.read() + + # Save file + with open(f"uploads/{file.filename}", "wb") as f: + f.write(contents) + + return {"filename": file.filename, "size": len(contents)} +``` + +## 模板 + +### 问:有哪些可用模板? + +**答:** FastAPI-fastkit 自带多个预制模板: + +
+ +```console +$ fastkit list-templates + Available Templates +┌─────────────────────────┬───────────────────────────────────┐ +│ fastapi-default │ Simple FastAPI Project │ +│ fastapi-async-crud │ Async Item Management API Server │ +│ fastapi-custom-response │ Custom Response System │ +│ fastapi-dockerized │ Dockerized FastAPI API │ +│ fastapi-empty │ Minimal FastAPI Project │ +│ fastapi-mcp │ MCP (Model Context Protocol) API │ +│ fastapi-psql-orm │ PostgreSQL FastAPI API │ +│ fastapi-single-module │ Single-file FastAPI Project │ +└─────────────────────────┴───────────────────────────────────┘ +``` + +
+ +### 问:如何使用指定模板? + +**答:** 使用 `startdemo` 命令: + +
+ +```console +$ fastkit startdemo +Enter the project name: my-blog +Select template: fastapi-psql-orm +``` + +
+ +### 问:可以创建自定义模板吗? + +**答:** 可以!创建目录结构并使用模板变量: + +``` +my-template/ +├── src/ +│ └── main.py-tpl +├── requirements.txt-tpl +└── template.yaml +``` + +```python +# main.py-tpl +from fastapi import FastAPI + +app = FastAPI(title="{{PROJECT_NAME}}") + +@app.get("/") +def read_root(): + return {"message": "Hello from {{PROJECT_NAME}}!"} +``` + +### 问:如何修改已有模板? + +**答:** 模板位于 `fastapi_project_template` 目录,您可以: + +1. **fork 仓库** 并修改模板 +2. **基于现有模板创建自定义模板** +3. **在创建项目后覆盖特定文件** + +## 开发服务器 + +### 问:如何启动开发服务器? + +**答:** 在项目目录中使用 `runserver` 命令: + +
+ +```console +$ cd my-project +$ source .venv/bin/activate # Activate virtual environment +$ fastkit runserver +INFO: Uvicorn running on http://127.0.0.1:8000 +``` + +
+ +### 问:服务器无法启动 —— 「Address already in use」 + +**答:** 8000 端口被占用。换一个端口,或结束已有进程: + +
+ +```console +# Use different port +$ fastkit runserver --port 8080 + +# Or find and kill existing process +$ lsof -ti:8000 | xargs kill -9 + +# On Windows +$ netstat -ano | findstr :8000 +$ taskkill /PID /F +``` + +
+ +### 问:自动重载不生效 + +**答:** 确认您处在项目目录中,并已激活虚拟环境: + +
+ +```console +# Check current directory +$ pwd +/path/to/my-project + +# Check virtual environment +$ which python +/path/to/my-project/.venv/bin/python + +# Start with explicit reload +$ fastkit runserver --reload +``` + +
+ +### 问:生产环境下如何配置服务器? + +**答:** **不要**在生产环境使用开发服务器,而应: + +```python +# Use gunicorn or similar WSGI server +$ pip install gunicorn +$ gunicorn src.main:app -w 4 -k uvicorn.workers.UvicornWorker + +# Or use Docker with the fastapi-dockerized template +$ fastkit startdemo # Select fastapi-dockerized +$ docker build -t my-app . +$ docker run -p 8000:8000 my-app +``` + +## 性能与优化 + +### 问:如何提升 API 性能? + +**答:** 多种优化策略: + +1. 对 I/O 操作使用 **async/await** +2. 为开销大的操作**加缓存** +3. **优化数据库查询** +4. 用**后台任务**处理重活 + +```python +# Async endpoint +@router.get("/users/{user_id}") +async def get_user(user_id: int): + user = await users_service.get_user_async(user_id) + return user + +# Background task +from fastapi import BackgroundTasks + +@router.post("/send-email") +def send_email(background_tasks: BackgroundTasks, email: str): + background_tasks.add_task(send_notification_email, email) + return {"message": "Email will be sent in background"} +``` + +### 问:如何加入缓存? + +**答:** 使用 Redis 做缓存: + +```python +import redis +from functools import wraps + +redis_client = redis.Redis(host='localhost', port=6379, db=0) + +def cache_result(expiration: int = 300): + def decorator(func): + @wraps(func) + async def wrapper(*args, **kwargs): + cache_key = f"{func.__name__}:{hash(str(args) + str(kwargs))}" + + # Try to get from cache + cached = redis_client.get(cache_key) + if cached: + return json.loads(cached) + + # Execute function and cache result + result = await func(*args, **kwargs) + redis_client.setex(cache_key, expiration, json.dumps(result)) + return result + return wrapper + return decorator + +@cache_result(expiration=600) +async def get_expensive_data(): + # Expensive operation + return complex_calculation() +``` + +### 问:如何处理大量并发请求? + +**答:** 使用合适的服务器配置: + +
+ +```console +# Development +$ fastkit runserver --workers 1 # Single worker for development + +# Production +$ gunicorn src.main:app -w 4 -k uvicorn.workers.UvicornWorker +$ uvicorn src.main:app --workers 4 --host 0.0.0.0 --port 8000 +``` + +
+ +## 测试 + +### 问:如何运行测试? + +**答:** 在项目目录中使用 pytest: + +
+ +```console +$ cd my-project +$ source .venv/bin/activate +$ python -m pytest + +# With coverage +$ python -m pytest --cov=src + +# Specific test file +$ python -m pytest tests/test_users.py + +# With verbose output +$ python -m pytest -v +``` + +
+ +### 问:如何编写 API 测试? + +**答:** 使用 FastAPI 的 test client: + +```python +from fastapi.testclient import TestClient +from src.main import app + +client = TestClient(app) + +def test_create_user(): + response = client.post( + "/api/v1/users/", + json={"email": "test@example.com", "username": "testuser"} + ) + assert response.status_code == 201 + assert response.json()["email"] == "test@example.com" + +def test_get_user(): + response = client.get("/api/v1/users/1") + assert response.status_code == 200 +``` + +### 问:如何对外部依赖进行 mock? + +**答:** 使用 pytest fixture 与 mock: + +```python +import pytest +from unittest.mock import Mock, patch + +@pytest.fixture +def mock_database(): + with patch('src.database.get_db') as mock_db: + mock_db.return_value = Mock() + yield mock_db + +def test_user_creation_with_mock_db(mock_database): + # Test with mocked database + response = client.post("/api/v1/users/", json=user_data) + assert response.status_code == 201 +``` + +## 参与贡献 + +### 问:如何为 FastAPI-fastkit 做贡献? + +**答:** 按以下步骤即可: + +1. 在 GitHub 上 **fork 仓库** +2. **配置开发环境** +3. **创建特性分支** +4. **进行改动**并附上测试 +5. **提交 Pull Request** + +
+ +```console +$ git clone https://github.com/yourusername/FastAPI-fastkit.git +$ cd FastAPI-fastkit +$ make dev-setup # Set up development environment +$ git checkout -b feature/my-feature +# Make changes... +$ make dev-check # Format, lint, and test +$ git commit -m "feat: add new feature" +$ git push origin feature/my-feature +``` + +
+ +### 问:Pull Request 应当包含什么? + +**答:** 每个 Pull Request 都应包含: + +- [ ] **对改动的清晰描述** +- [ ] 新功能对应的**测试** +- [ ] 必要时更新**文档** +- [ ] **遵循代码规范** +- [ ] **所有检查通过** + +### 问:如何报告 bug? + +**答:** 在 GitHub 上创建 issue,包含: + +1. **bug 描述**与预期行为 +2. **复现步骤** +3. **环境信息**(操作系统、Python 版本等) +4. **错误信息**或日志 +5. **最小可复现示例**(如可能) + +### 问:如何提交新特性请求? + +**答:** 打开一个特性请求 issue,包含: + +1. 对特性的**清晰描述** +2. **使用场景**与动机 +3. **建议的实现方式**(可选) +4. **类似特性的例子** + +## 故障排查 + +### 问:出现 import 错误 + +**答:** 检查您的 Python path 与虚拟环境: + +
+ +```console +# Check virtual environment is activated +$ which python +/path/to/project/.venv/bin/python + +# Check Python path +$ python -c "import sys; print(sys.path)" + +# Reinstall in editable mode (for development) +$ pip install -e . +``` + +
+ +### 问:数据库连接问题 + +**答:** 对于带数据库的模板,请确认数据库已运行: + +
+ +```console +# PostgreSQL template +$ docker-compose up -d postgres # Start database +$ alembic upgrade head # Run migrations + +# Check connection +$ docker-compose logs postgres +``` + +
+ +### 问:找不到模板文件 + +**答:** 这通常意味着模板路径有问题: + +
+ +```console +# Check available templates +$ fastkit list-templates + +# Check template directory +$ python -c "import fastapi_fastkit; print(fastapi_fastkit.__path__)" + +# Reinstall if templates missing +$ pip uninstall fastapi-fastkit +$ pip install fastapi-fastkit +``` + +
+ +### 问:pre-commit 钩子失败 + +**答:** 安装并运行钩子: + +
+ +```console +$ pip install pre-commit +$ pre-commit install +$ pre-commit run --all-files + +# Fix formatting issues +$ black src/ tests/ +$ isort src/ tests/ +``` + +
+ +### 问:CI 测试失败,但本地通过 + +**答:** 常见原因与解决办法: + +1. **环境差异**:核对 Python 版本是否一致 +2. **缺少依赖**:确认测试依赖已安装 +3. **路径问题**:使用绝对 import +4. **时序问题**:在异步测试中加入合理的等待 + +
+ +```console +# Test with same Python version as CI +$ python3.12 -m pytest + +# Check for missing dependencies +$ pip install -r requirements-dev.txt + +# Run tests in isolated environment +$ tox +``` + +
+ +## 获取帮助 + +### 问:在哪里获取帮助? + +**答:** 多种渠道: + +- **GitHub Issues**:报告 bug 与请求特性 +- **GitHub Discussions**:提问与社区交流 +- **文档**:用户指南与教程 +- **代码示例**:查看已有模板与测试 + +### 问:如何掌握最新动态? + +**答:** 关注项目更新: + +- 在 GitHub 上 **watch 仓库** +- 通过 **releases** 了解新特性 +- 阅读 **changelog**,关注破坏性变更 +- 在文档中**遵循最佳实践** + +!!! tip "进阶小贴士" + - 始终在 Python 项目中使用虚拟环境 + - 保持 FastAPI-fastkit 安装为最新 + - 用 `fastkit --help` 查看可用命令 + - 卡住时回到文档查阅 + - 不要犹豫,在 GitHub Discussions 上多多提问 diff --git a/docs/zh/reference/preset-feature-matrix.md b/docs/zh/reference/preset-feature-matrix.md new file mode 100644 index 0000000..46fc712 --- /dev/null +++ b/docs/zh/reference/preset-feature-matrix.md @@ -0,0 +1,60 @@ +# 架构预设 / 功能矩阵 + +交互式的 `fastkit init --interactive` 在收集功能选择之前,会先询问一个 **架构预设**([issue #44](https://github.com/bnbong/FastAPI-fastkit/issues/44))。预设决定了生成项目的整体布局 —— 不同的预设会使用不同的基础模板,并把生成的配置文件落到不同位置,使其与已有结构相邻,而不是放进平行的 `src/config/` 子树中。 + +本页是关于「每个预设的作用、文件落地位置、哪些功能组合需要手动接入」的权威说明。 + +## 预设 → 基础模板 + +| 预设 | 基础模板 | 描述 | +|---|---|---| +| `minimal` | `fastapi-empty` | 最小可运行的 FastAPI 应用 —— 占位的 `main.py` 会根据您的功能选择重新生成。 | +| `single-module` | `fastapi-single-module` | 单文件 FastAPI 应用 —— `main.py` 会被重新生成。 | +| `classic-layered` | `fastapi-default` | 分层布局(`api/routes`、`crud`、`schemas`、`core`)。自带的 `main.py` 会被保留。 | +| `domain-starter` | `fastapi-domain-starter` | 领域驱动布局(`src/app/domains//`)。自带的 `main.py` 会被保留。**推荐默认选项。** | + +## 生成文件的落地位置 + +| 预设 | `main.py` 覆盖策略 | 数据库配置落点 | 认证配置落点 | +|---|---|---|---| +| `minimal` | 在 `src/main.py` 处重新生成 | `src/config/database.py` | `src/config/auth.py` | +| `single-module` | 在 `src/main.py` 处重新生成 | `src/config/database.py` | `src/config/auth.py` | +| `classic-layered` | 保留(模板自带) | `src/core/database.py` | `src/core/auth.py` | +| `domain-starter` | 保留(模板自带) | `src/app/core/database.py` | `src/app/core/auth.py` | + +## 各预设对数据库 / 认证功能的支持 + +下列功能在**所有**预设中都支持 —— 包安装都会成功;差别只在于动态的 `main.py` 覆盖是否会自动接入它们。 + +| 功能 | `minimal` / `single-module` | `classic-layered` / `domain-starter` | +|---|---|---| +| **数据库**(PostgreSQL、MySQL、SQLite、MongoDB) | 生成配置模块,**并且**在重新生成的 `main.py` 中插入 `await init_db()` 调用。 | 在预设对应路径生成配置模块。自带的 `main.py` 会**保留**,因此需要手动把 `get_db()` 接入路由器。 | +| **认证**(JWT、FastAPI-Users、OAuth2、基于会话) | 生成认证配置模块。JWT 还会在重新生成的 `main.py` 中导入 `HTTPBearer`。 | 在预设对应路径生成认证配置模块。不会向 `main.py` 添加 import —— 需要手动接入依赖。 | +| **后台任务**(Celery、Dramatiq) | 安装相应包;目前没有 main.py 覆盖。 | 同上。 | +| **缓存**(Redis) | 安装相应包;目前没有 main.py 覆盖。 | 同上。 | +| **CORS**(工具类) | 在重新生成的 `main.py` 中加入 `CORSMiddleware`,`allow_origins=['*']`。 | 自带的 `main.py` 中**已接入**(以 `settings.all_cors_origins` 作为条件)。只需在 `.env` 中设置 `BACKEND_CORS_ORIGINS` 即可启用 —— 无需修改代码。 | +| **测试**(基础 / 覆盖率 / 进阶) | 在项目根生成 `pytest.ini`。 | 同上。 | +| **部署**(Docker、docker-compose) | 在项目根写入 `Dockerfile` 和/或 `docker-compose.yml`。 | 同上。 | + +## 何时会出现「Preset compatibility」警告 + +对于**保留自带 `main.py`** 的预设(`classic-layered`、`domain-starter`),某些功能选择不会被自动接入到应用。CLI 会在生成结束时一次性给出警告,列出哪些选择需要手动接入: + +| 选择的功能 | 在 `classic-layered` / `domain-starter` 下是否触发警告? | +|---|---| +| `CORS`(工具类) | ❌ —— 自带的 `main.py` 已接入。只需在 `.env` 中填好 `BACKEND_CORS_ORIGINS`。 | +| `Rate-Limiting`(工具类) | ✅ —— 不会加入 `slowapi` 限流器的初始化 | +| `Prometheus`(监控) | ✅ —— 不会调用 `Instrumentator().instrument(app)` | +| 任意数据库 / 认证选择 | ⚠️ —— 配置文件已生成,但需要您手动在路由器中通过 `Depends()` 接入 | + +对 `minimal` 与 `single-module` 预设,动态的 `main.py` 覆盖会自动处理 CORS、限流与 Prometheus 监控,因此不会触发警告。 + +## 不支持的组合(安全起见) + +策略设计者**刻意不会**尝试把生成的代码拼接进模板自带的 `main.py`。这样做有可能产生坏掉的 import 或重复的路由器。整体契约是: + +- 所选的包总是会被安装(这样 `pip freeze` 与用户意图匹配)。 +- 生成的配置模块总是落到对应预设的路径。 +- 对于保留 main 的预设,会告知用户哪些选择仍需手动接入,而不是产出悄无声息坏掉的代码。 + +如果您需要对所有功能都进行自动接入,请选择 `minimal` 或 `single-module` —— 它们会基于功能开关重新生成 `main.py`。 diff --git a/docs/zh/reference/template-quality-assurance.md b/docs/zh/reference/template-quality-assurance.md new file mode 100644 index 0000000..f6ffa83 --- /dev/null +++ b/docs/zh/reference/template-quality-assurance.md @@ -0,0 +1,219 @@ +# 模板质量保障 + +FastAPI-fastkit 提供了完善的自动化模板校验,确保所有模板在不同环境与包管理器下都保持高质量且可正常工作。 + +## 多层质量保障 + +FastAPI-fastkit 同时使用**两套互补的质量保障系统**: + +### 1. 静态模板检查 +**对模板结构与语法的每周自动校验** + +### 2. 动态模板测试 +**包含真实项目创建的完整端到端测试** + +## 自动化的每周检查 + +每周三午夜(UTC),我们的 GitHub Actions 工作流会自动检查所有 FastAPI 模板,确保它们达到质量标准: + +- ✅ **文件结构校验** —— 确保所有必备文件与目录都存在 +- ✅ **文件扩展名核对** —— 确认模板文件使用正确的 `.py-tpl` 扩展名 +- ✅ **依赖检查** —— 确认 FastAPI 及必要依赖被正确声明 +- ✅ **FastAPI 实现** —— 确认模板包含合规的 FastAPI 应用初始化 +- ✅ **测试执行** —— 运行模板的测试以确认功能正常 + +## 自动化模板测试系统 + +FastAPI-fastkit 自带一套**革新性的自动化测试系统**,对每个模板进行完整校验: + +### 动态模板发现 + +测试系统会**自动发现所有模板**,无需手动配置: + +```console +# Test all templates automatically +$ pytest tests/test_templates/test_all_templates.py -v + +# Results show all discovered templates +PASSED tests/test_templates/test_all_templates.py::TestAllTemplates::test_template_creation[fastapi-default] +PASSED tests/test_templates/test_all_templates.py::TestAllTemplates::test_template_creation[fastapi-async-crud] +PASSED tests/test_templates/test_all_templates.py::TestAllTemplates::test_template_creation[fastapi-dockerized] +PASSED tests/test_templates/test_all_templates.py::TestAllTemplates::test_template_creation[fastapi-psql-orm] +``` + +### 全面的测试覆盖 + +每个模板都会经历**完整的端到端测试**: + +#### ✅ 项目创建过程 +- 模板复制与文件转换 +- 项目元数据注入(名称、作者、描述) +- 文件结构校验 + +#### ✅ 包管理器兼容性 +- **UV**(默认):基于 Rust 的高速包管理器 +- **PDM**:现代 Python 依赖管理工具 +- **Poetry**:成熟的依赖管理工具 +- **PIP**:传统的 Python 包管理器 + +#### ✅ 虚拟环境管理 +- 为每种包管理器创建对应环境 +- 校验依赖安装 +- 处理包管理器特有的工作流 + +#### ✅ 依赖解析 +- 生成 `pyproject.toml`(UV、PDM、Poetry) +- 生成 `requirements.txt`(PIP) +- 元数据合规(PEP 621) +- 构建系统配置 + +#### ✅ 项目结构校验 +- FastAPI 项目识别 +- 必备文件存在性 +- 目录结构核对 + +### 测试执行示例 + +**运行全部模板测试:** +```console +$ pytest tests/test_templates/test_all_templates.py -v +``` + +**测试指定模板:** +```console +$ pytest tests/test_templates/test_all_templates.py::TestAllTemplates::test_template_creation[fastapi-default] -v +``` + +**在 PDM 环境中运行测试:** +```console +$ pdm run pytest tests/test_templates/test_all_templates.py -v +``` + +### 持续集成 + +自动化测试系统在 **CI/CD 流水线**中运行: + +- ✅ **Pull Request 校验**:每个 PR 都会测试受影响的模板 +- ✅ **每日夜间测试**:对完整模板套件进行校验 +- ✅ **包管理器测试**:覆盖所有包管理器的交叉校验 +- ✅ **环境测试**:覆盖多个 Python 版本与平台 + +### 对贡献者的好处 + +**零配置测试:** + +- 🚀 添加新模板 → 自动测试 +- ⚡ 无需手动创建测试文件 +- 🛡️ 一致的质量标准 + +**全面覆盖:** + +- 🔍 端到端项目创建测试 +- 📦 多包管理器校验 +- 🏗️ 完整的依赖解析测试 +- ✅ 模拟真实使用场景 + +**开发体验:** + +- 🎯 **专注模板内容**:测试由系统自动完成 +- 🔄 **即时反馈**:测试执行迅速 +- 📊 **结果清晰**:报告信息详尽 +- 🚫 **零样板代码**:无需任何测试配置 + +## 手动模板检查 + +为开发与调试之便,您可以通过本地检查脚本或 Makefile 命令手动检查模板: + +### 直接使用检查脚本 + +```console +# Inspect all templates +$ python scripts/inspect-templates.py + +# Inspect specific templates +$ python scripts/inspect-templates.py --templates fastapi-default,fastapi-async-crud + +# Verbose output with detailed information +$ python scripts/inspect-templates.py --verbose + +# Save results to custom file +$ python scripts/inspect-templates.py --output my_results.json +``` + +### 使用 Makefile 命令 + +```console +# Inspect all templates +$ make inspect-templates + +# Inspect with verbose output +$ make inspect-templates-verbose + +# Inspect specific templates +$ make inspect-template TEMPLATES="fastapi-default,fastapi-async-crud" +``` + +## 检查结果 + +- **成功的检查**会被记录在工作流输出与产物中 +- **失败的检查**会自动创建 GitHub issue,附带详细的错误报告 +- **检查历史**在 GitHub Actions 产物中保留 30 天 + +## 理解检查输出 + +运行模板检查时,您会看到类似输出: + +```console +📋 Found 6 templates to inspect: fastapi-async-crud, fastapi-custom-response, fastapi-default, fastapi-dockerized, fastapi-empty, fastapi-psql-orm +============================================================ +🔍 Inspecting template: fastapi-async-crud + Path: /path/to/src/fastapi_fastkit/fastapi_project_template/fastapi-async-crud +✅ fastapi-async-crud: PASSED +---------------------------------------- +🔍 Inspecting template: fastapi-custom-response + Path: /path/to/src/fastapi_fastkit/fastapi_project_template/fastapi-custom-response +✅ fastapi-custom-response: PASSED +---------------------------------------- +... +============================================================ +📊 INSPECTION SUMMARY + Total templates: 6 + ✅ Passed: 6 + ❌ Failed: 0 +🎉 All templates passed inspection! +📄 Results saved to: template_inspection_results.json +``` + +## 模板要求 + +要让模板通过检查,必须满足以下要求: + +### 文件结构 +- 必须包含一个带 Python 源文件的 `src/` 目录 +- Python 文件必须使用 `.py-tpl` 扩展名 +- 必须包含 `tests/` 目录与 `README.md-tpl` 文件 +- 必须至少包含**一份**元数据文件: + - `pyproject.toml-tpl`(推荐,PEP 621),或 + - `setup.py-tpl`(遗留,仍可接受) +- 当 `pyproject.toml-tpl` 已声明 `[project].dependencies` 时,`requirements.txt-tpl` 为可选 + +### FastAPI 要求 +- 必须包含 FastAPI 应用初始化 +- 必须在以下至少一个位置把 `fastapi` 声明为依赖:`pyproject.toml-tpl` 的 `[project].dependencies`、`requirements.txt-tpl`,或 `setup.py-tpl` 的 `install_requires` +- 所有模板文件必须具有有效的 Python 语法 + +### 身份标识 + +模板应携带 FastAPI-fastkit 的身份标识,这样生成的项目在用户工作区中可以与无关的 FastAPI 项目区分开: + +- `pyproject.toml-tpl` —— 在 `description` 中带 `[FastAPI-fastkit templated]` 前缀,并提供带 `managed = true` 的 `[tool.fastapi-fastkit]` 表。 +- `setup.py-tpl` —— 在 `setup()` 的 `description` 参数中带 `[FastAPI-fastkit templated]` 前缀。 + +`is_fastkit_project()` 接受其中任一标识(pyproject 优先,setup.py 是遗留回退;匹配不区分大小写)。即使模板遗漏了它们,元数据注入也能确保生成的项目带上这些标识。 + +### 质量标准 +- 所有模板文件必须语法正确 +- 依赖必须正确声明 +- 模板结构必须遵循 FastAPI-fastkit 的约定 + +这套自动化的质量保障可以让所有模板保持可靠,并随时可用于生产。 diff --git a/docs/zh/reference/translation-status.md b/docs/zh/reference/translation-status.md new file mode 100644 index 0000000..ea9d6ef --- /dev/null +++ b/docs/zh/reference/translation-status.md @@ -0,0 +1,82 @@ +# 翻译状态 + +FastAPI-fastkit 以多种语言发布文档,但这些翻译**并不总是完全同步**。本页面会说明哪些内容已经翻译、未翻译时会显示什么,以及您可以如何参与补充。 + +## 权威来源 + +> **英文(`en`)是权威来源。** 文档中描述的所有产品、CLI 与 API 行为,都先在英文文件中编写。其他语言是这些英文源的翻译,可能落后于某个版本。 +> +> 如果某个翻译页面与英文页面不一致,**请以英文页面为准**,直到翻译跟上为止。 + +英文文件位于 [`docs/en/`](https://github.com/bnbong/FastAPI-fastkit/tree/main/docs/en)。所有其他语言(`docs/ko/`、`docs/ja/`,……)都是翻译目标。 + +仓库根目录的 `CHANGELOG.md` 也属于这份英文权威来源。各语言的 `changelog.md` 页面可以作为入口页或包装页存在,但会刻意复用规范的英文版本历史,而不是维护一份独立的翻译版本。 + +## 各语言的完成度 + +下表中的数字是各语言目录中的 Markdown 页面数,相对于英文源的比例。它们反映的是仓库中实际存在的内容 —— 而不是语言切换器中显示的样子(下一节会解释)。 + +| 语言 | 状态 | Markdown 页面数 | 备注 | +|---|---|---:|---| +| 🇬🇧 英文(`en`) | ✅ 权威来源 | 26 / 26 | 标准来源。 | +| 🇰🇷 韩文(`ko`) | ✅ 完成 | 26 / 26 | 该语言的所有页面都已存在。Phase 1:顶层页面 + 核心用户指南;Phase 2:其余用户指南 + 全部教程;Phase 3:贡献文档 + 参考文档。`docs/ko/changelog.md` 刻意复用规范的英文 `CHANGELOG.md`。 | +| 🇯🇵 日文(`ja`) | ✅ 完成 | 26 / 26 | 该语言的所有页面都已存在。Phase 1:顶层页面 + 核心用户指南;Phase 2:其余用户指南 + 全部教程;Phase 3:贡献文档 + 参考文档。`docs/ja/changelog.md` 刻意复用规范的英文 `CHANGELOG.md`。 | +| 🇨🇳 中文(`zh`) | ✅ 完成 | 26 / 26 | 该语言的所有页面都已存在。Phase 1:顶层页面 + 核心用户指南;Phase 2:其余用户指南 + 全部教程;Phase 3:贡献文档 + 参考文档。`docs/zh/changelog.md` 刻意复用规范的英文 `CHANGELOG.md`。 | +| 🇪🇸 西班牙文(`es`) | ✅ 完成 | 26 / 26 | 该语言的所有页面都已存在。Phase 1:顶层页面 + 核心用户指南;Phase 2:其余用户指南 + 全部教程;Phase 3:贡献文档 + 参考文档。`docs/es/changelog.md` 刻意复用规范的英文 `CHANGELOG.md`。 | +| 🇫🇷 法文(`fr`) | ✅ 完成 | 26 / 26 | 该语言的所有页面都已存在。Phase 1:顶层页面 + 核心用户指南;Phase 2:其余用户指南 + 全部教程;Phase 3:贡献文档 + 参考文档。`docs/fr/changelog.md` 刻意复用规范的英文 `CHANGELOG.md`。 | +| 🇩🇪 德文(`de`) | ✅ 完成 | 26 / 26 | 该语言的所有页面都已存在。Phase 1:顶层页面 + 核心用户指南;Phase 2:其余用户指南 + 全部教程;Phase 3:贡献文档 + 参考文档。`docs/de/changelog.md` 刻意复用规范的英文 `CHANGELOG.md`。 | + +*快照验证时间:2026-06-18;在 Phase 3(贡献文档 + 参考文档)完成后,已基于当前分支重新统计 `zh` 这一行。中文目前 26 个本地化页面全部就位,状态记为 ✅ 完成。* 此表通过人工维护;如需在仓库根目录重新统计当前状态,请运行: + +```console +$ for loc in en ko ja zh es fr de; do + echo "$loc: $(find docs/$loc -name '*.md' 2>/dev/null | wc -l | tr -d ' ')" + done +``` + +若重新统计的结果与表格不一致,说明表格已过时 —— 请更新它(或开 PR / issue 报告差异)。 + +图例: + +- ✅ **权威来源** —— 我们以此语言为基准撰写。 +- 🟡 **部分翻译** —— 已翻译部分页面;缺失的页面会回退到英文。 +- 🔴 **骨架** —— 语言切换器中存在该入口,但尚未提交任何翻译过的页面。站点会在翻译后的导航标签下渲染英文内容。 + +## 回退机制如何工作 + +文档站点使用 [`mkdocs-static-i18n`](https://github.com/ultrabug/mkdocs-static-i18n) 并开启 `fallback_to_default: true`。这意味着: + +- 对每个翻译语言,MkDocs 只为该语言目录中存在的页面生成输出。 +- 对每个**不存在于**某语言的页面,构建会回退到该页面的英文版本。 +- 全站语言切换器始终列出所有已配置的语言,而不论每个语言实际包含多少页面 —— 因为构建会为每种情况生成可访问的 URL(本语言页面 → 必要时回退到英文)。 + +所以,语言切换器中的 🔴 骨架条目**并不**意味着文档已经翻译完成 —— 它只表示该语言的构建目标已经配置好。这样设计是有意为之(外部贡献者可以逐页翻译,而不会破坏链接结构),但也确实会让语言切换器看起来比实际内容更完整。 + +## 如何阅读站点 + +- **默认使用英文**,若您希望获得最准确、最新的信息。 +- **先查看本页确认状态,再决定是否使用翻译语言**。如果状态是 🟡 或 🔴,而您打开了尚未翻译的主题,实际看到的会是英文回退内容,只是外层仍显示翻译后的导航标签。 + +## 如何提供帮助 + +当前的推进方式是:**每种语言对应一个跟踪 issue**,再把工作拆成若干**阶段(phase)**。例如 `ko` 会分为 Phase 1(顶层页面 + 核心用户指南)、Phase 2(其余用户指南 + 全部教程)和 Phase 3(贡献文档 + 参考文档)。每个阶段都会以独立 PR 落地,这样审阅者可以聚焦于一个完整的小范围,不必等到整套翻译全部完成。 + +如果您想贡献: + +1. 阅读 [翻译指南](../contributing/translation-guide.md),了解工作流程、工具与风格约定。 +2. **优先查找或创建对应语言的跟踪 issue。** 若该语言已有一个开放的跟踪 issue,请在其中认领某个 phase(或 phase 内某个具体页面),以避免重复工作。如果尚不存在跟踪 issue,请创建一个,列出每个 phase 所属的页面,然后从 Phase 1 开始。 +3. **首选每个 phase 对应一份 PR**。更小的“只修这一页”PR 同样欢迎 —— 尤其适合修复与英文不同步的内容 —— 但对于全新的语言翻译工作,按 phase 打包更有利于保持术语和交叉链接措辞的一致性。 +4. 提交 PR,将文件加入 `docs//<相同路径>` 下。请保持文件名与英文源完全一致,这样 MkDocs 才会自动识别。 +5. 将本地化的 changelog 页面视作规范英文 `CHANGELOG.md` 的包装页或入口页,除非项目政策以后明确调整。 +6. 更新本页表格以反映新的完成度(可使用本页顶部的重新统计片段),并同步更新“快照验证时间”,方便审阅者确认上次对齐的时间点。如果该语言仍处于部分翻译状态,请在“备注”列注明已经完成到哪个 phase。 + +也欢迎报告翻译页面与英文源失同步的缺陷 —— 请同时附上英文页面与翻译页面的链接,便于分流。 + +## 为何还要保留 🔴 骨架语言 + +两个原因: + +1. **可预测的 URL 结构。** 每种语言的 `//` 子树都已可访问,这样翻译页面一旦落地,链接从第一天起就是稳定的 —— 包括本指南里已经发布过的链接。 +2. **降低贡献门槛。** 只翻译单页的贡献者无需再去修改 MkDocs 配置接入新的语言构建目标 —— 直接把文件放到对应位置即可。 + +如果某个语言长期处于 🔴 骨架且没有任何贡献,我们会重新评估是否保留它的构建目标。该决定会单独跟踪,**不会**由这个状态页面悄悄改变。 diff --git a/docs/zh/tutorial/async-crud-api.md b/docs/zh/tutorial/async-crud-api.md new file mode 100644 index 0000000..a183ca9 --- /dev/null +++ b/docs/zh/tutorial/async-crud-api.md @@ -0,0 +1,665 @@ +# 构建异步 CRUD API + +学习如何利用 FastAPI 的异步处理能力构建高性能的 CRUD API。本教程中我们会使用 `fastapi-async-crud` 模板实现异步文件 I/O 与高效的数据处理。 + +## 您将学到的内容 + +- 理解异步 FastAPI 应用 +- 使用 `async/await` 语法的异步 CRUD 操作 +- 使用 aiofiles 处理异步文件 +- 编写并运行异步测试 +- 性能优化技巧 + +## 前置条件 + +- 完成 [基础 API 服务器教程](basic-api-server.md) +- 理解 Python `async/await` 的基本概念 +- 已安装 FastAPI-fastkit + +## 为什么需要异步处理 + +让我们先理解同步与异步处理之间的差异: + +### 同步处理 + +```python +def process_items(): + item1 = read_file("item1.json") # Wait 2 seconds + item2 = read_file("item2.json") # Wait 2 seconds + item3 = read_file("item3.json") # Wait 2 seconds + return [item1, item2, item3] # Total: 6 seconds +``` + +### 异步处理 + +```python +async def process_items(): + item1_task = read_file_async("item1.json") # Start concurrently + item2_task = read_file_async("item2.json") # Start concurrently + item3_task = read_file_async("item3.json") # Start concurrently + + items = await asyncio.gather(item1_task, item2_task, item3_task) + return items # Total: 2 seconds +``` + +## 第 1 步:创建异步 CRUD 项目 + +使用 `fastapi-async-crud` 模板创建项目: + +
+ +```console +$ fastkit startdemo fastapi-async-crud +Enter the project name: async-todo-api +Enter the author name: Developer Kim +Enter the author email: developer@example.com +Enter the project description: Asynchronous todo management API +Deploying FastAPI project using 'fastapi-async-crud' template + + Project Information +┌──────────────┬─────────────────────────────────────────┐ +│ Project Name │ async-todo-api │ +│ Author │ Developer Kim │ +│ Author Email │ developer@example.com │ +│ Description │ Asynchronous todo management API │ +└──────────────┴─────────────────────────────────────────┘ + + Template Dependencies +┌──────────────┬───────────────────┐ +│ Dependency 1 │ fastapi │ +│ Dependency 2 │ uvicorn │ +│ Dependency 3 │ pydantic │ +│ Dependency 4 │ pydantic-settings │ +│ Dependency 5 │ aiofiles │ +│ Dependency 6 │ pytest-asyncio │ +└──────────────┴───────────────────┘ + +Select package manager (pip, uv, pdm, poetry) [uv]: uv +Do you want to proceed with project creation? [y/N]: y + +✨ FastAPI project 'async-todo-api' from 'fastapi-async-crud' has been created successfully! +``` + +
+ +## 第 2 步:分析项目结构 + +来看看生成后的项目有哪些关键差异: + +``` +async-todo-api/ +├── src/ +│ ├── main.py # 异步 FastAPI 应用入口 +│ ├── api/ +│ │ └── routes/ +│ │ └── items.py # 异步 CRUD 端点 +│ ├── crud/ +│ │ └── items.py # 异步数据处理逻辑 +│ ├── schemas/ +│ │ └── items.py # 数据模型(结构相同) +│ ├── mocks/ +│ │ └── mock_items.json # JSON 文件数据库 +│ └── core/ +│ └── config.py # 配置文件 +└── tests/ + ├── conftest.py # 异步测试配置 + └── test_items.py # 异步测试用例 +``` + +### 关键差异 + +1. **aiofiles**:异步文件 I/O 处理 +2. **pytest-asyncio**:对异步测试的支持 +3. **async/await 模式**:所有 CRUD 操作都用异步方式实现 + +## 第 3 步:理解异步 CRUD 逻辑 + +### 异步数据处理(`src/crud/items.py`) + +```python +import json +import asyncio +from typing import List, Optional +from aiofiles import open as aio_open +from pathlib import Path + +from src.schemas.items import Item, ItemCreate, ItemUpdate + +class AsyncItemCRUD: + def __init__(self, data_file: str = "src/mocks/mock_items.json"): + self.data_file = Path(data_file) + + async def _read_data(self) -> List[dict]: + """异步读取 JSON 文件中的数据""" + try: + async with aio_open(self.data_file, 'r', encoding='utf-8') as f: + content = await f.read() + return json.loads(content) + except FileNotFoundError: + return [] + + async def _write_data(self, data: List[dict]) -> None: + """异步将数据写入 JSON 文件""" + async with aio_open(self.data_file, 'w', encoding='utf-8') as f: + await f.write(json.dumps(data, indent=2, ensure_ascii=False)) + + async def get_items(self) -> List[Item]: + """异步获取所有 items""" + data = await self._read_data() + return [Item(**item) for item in data] + + async def get_item(self, item_id: int) -> Optional[Item]: + """异步获取指定 item""" + data = await self._read_data() + item_data = next((item for item in data if item["id"] == item_id), None) + return Item(**item_data) if item_data else None + + async def create_item(self, item: ItemCreate) -> Item: + """异步创建新 item""" + data = await self._read_data() + new_id = max([item["id"] for item in data], default=0) + 1 + + new_item = Item(id=new_id, **item.dict()) + data.append(new_item.dict()) + + await self._write_data(data) + return new_item + + async def update_item(self, item_id: int, item_update: ItemUpdate) -> Optional[Item]: + """Update item (asynchronous)""" + data = await self._read_data() + + for i, item in enumerate(data): + if item["id"] == item_id: + update_data = item_update.dict(exclude_unset=True) + data[i].update(update_data) + await self._write_data(data) + return Item(**data[i]) + + return None + + async def delete_item(self, item_id: int) -> bool: + """Delete item (asynchronous)""" + data = await self._read_data() + original_length = len(data) + + data = [item for item in data if item["id"] != item_id] + + if len(data) < original_length: + await self._write_data(data) + return True + + return False +``` + +### 异步 API 端点(`src/api/routes/items.py`) + +```python +from typing import List +from fastapi import APIRouter, HTTPException, status + +from src.schemas.items import Item, ItemCreate, ItemUpdate +from src.crud.items import AsyncItemCRUD + +router = APIRouter() +crud = AsyncItemCRUD() + +@router.get("/", response_model=List[Item]) +async def read_items(): + """Retrieve all items (asynchronous)""" + return await crud.get_items() + +@router.get("/{item_id}", response_model=Item) +async def read_item(item_id: int): + """Retrieve specific item (asynchronous)""" + item = await crud.get_item(item_id) + if item is None: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=f"Item with id {item_id} not found" + ) + return item + +@router.post("/", response_model=Item, status_code=status.HTTP_201_CREATED) +async def create_item(item: ItemCreate): + """Create new item (asynchronous)""" + return await crud.create_item(item) + +@router.put("/{item_id}", response_model=Item) +async def update_item(item_id: int, item_update: ItemUpdate): + """Update item (asynchronous)""" + updated_item = await crud.update_item(item_id, item_update) + if updated_item is None: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=f"Item with id {item_id} not found" + ) + return updated_item + +@router.delete("/{item_id}", status_code=status.HTTP_204_NO_CONTENT) +async def delete_item(item_id: int): + """Delete item (asynchronous)""" + deleted = await crud.delete_item(item_id) + if not deleted: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=f"Item with id {item_id} not found" + ) +``` + +## 第 4 步:运行服务器并测试 + +进入项目目录运行服务器: + +
+ +```console +$ cd async-todo-api +$ fastkit runserver +Starting FastAPI server at 127.0.0.1:8000... + +INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) +INFO: Started reloader process [12345] using WatchFiles +INFO: Started server process [12346] +INFO: Waiting for application startup. +INFO: Application startup complete. +``` + +
+ +### 性能测试 + +我们来验证异步处理的性能,尝试同时发出多个请求: + +**并发请求测试(Python 脚本)** + +```python +import asyncio +import aiohttp +import time + +async def create_item(session, item_data): + async with session.post("http://127.0.0.1:8000/items/", json=item_data) as response: + return await response.json() + +async def test_concurrent_requests(): + start_time = time.time() + + items_to_create = [ + {"name": f"Item {i}", "description": f"Description {i}", "price": i * 10, "tax": i} + for i in range(1, 11) # Create 10 items concurrently + ] + + async with aiohttp.ClientSession() as session: + tasks = [create_item(session, item) for item in items_to_create] + results = await asyncio.gather(*tasks) + + end_time = time.time() + print(f"Created 10 items in: {end_time - start_time:.2f} seconds") + print(f"Number of items created: {len(results)}") + +# Run test +# asyncio.run(test_concurrent_requests()) +``` + +## 第 5 步:编写异步测试 + +### 测试配置(`tests/conftest.py`) + +```python +import pytest +import asyncio +from httpx import AsyncClient +from src.main import app + +@pytest.fixture(scope="session") +def event_loop(): + """Event loop configuration""" + loop = asyncio.get_event_loop_policy().new_event_loop() + yield loop + loop.close() + +@pytest.fixture +async def async_client(): + """Asynchronous test client""" + async with AsyncClient(app=app, base_url="http://test") as client: + yield client +``` + +### 异步测试用例(`tests/test_items.py`) + +```python +import pytest +from httpx import AsyncClient + +@pytest.mark.asyncio +async def test_create_item_async(async_client: AsyncClient): + """Asynchronous item creation test""" + item_data = { + "name": "Test Item", + "description": "Item for asynchronous testing", + "price": 100.0, + "tax": 10.0 + } + + response = await async_client.post("/items/", json=item_data) + + assert response.status_code == 201 + data = response.json() + assert data["name"] == item_data["name"] + assert data["price"] == item_data["price"] + assert "id" in data + +@pytest.mark.asyncio +async def test_read_items_async(async_client: AsyncClient): + """Asynchronous item list retrieval test""" + response = await async_client.get("/items/") + + assert response.status_code == 200 + items = response.json() + assert isinstance(items, list) + +@pytest.mark.asyncio +async def test_concurrent_operations(async_client: AsyncClient): + """Concurrent operations test""" + import asyncio + + # Create multiple items concurrently + tasks = [] + for i in range(5): + item_data = { + "name": f"ConcurrentItem{i}", + "description": f"Description{i}", + "price": i * 10, + "tax": i + } + task = async_client.post("/items/", json=item_data) + tasks.append(task) + + responses = await asyncio.gather(*tasks) + + # Verify all requests succeeded + for response in responses: + assert response.status_code == 201 + + # Verify created items + response = await async_client.get("/items/") + items = response.json() + assert len(items) >= 5 +``` + +### 运行测试 + +
+ +```console +$ pytest tests/ -v --asyncio-mode=auto +======================== test session starts ======================== +collected 8 items + +tests/test_items.py::test_create_item_async PASSED [ 12%] +tests/test_items.py::test_read_items_async PASSED [ 25%] +tests/test_items.py::test_read_item_async PASSED [ 37%] +tests/test_items.py::test_update_item_async PASSED [ 50%] +tests/test_items.py::test_delete_item_async PASSED [ 62%] +tests/test_items.py::test_concurrent_operations PASSED [ 75%] +tests/test_items.py::test_item_not_found_async PASSED [ 87%] +tests/test_items.py::test_invalid_item_data_async PASSED [100%] + +======================== 8 passed in 0.24s ======================== +``` + +
+ +## 第 6 步:性能监控与优化 + +### 添加响应耗时测量中间件 + +在 `src/main.py` 中加入性能监控: + +```python +import time +from fastapi import FastAPI, Request +from src.api.api import api_router +from src.core.config import settings + +app = FastAPI( + title=settings.PROJECT_NAME, + version=settings.VERSION, + description=settings.DESCRIPTION, +) + +@app.middleware("http") +async def add_process_time_header(request: Request, call_next): + """Add request processing time to headers""" + 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 + +app.include_router(api_router) + +@app.get("/") +async def read_root(): + return {"message": "Welcome to the Asynchronous Todo API!"} +``` + +### 实现异步批量处理 + +我们加上批量端点,以便一次处理多个 item: + +```python +# Add to src/api/routes/items.py + +@router.post("/batch", response_model=List[Item]) +async def create_items_batch(items: List[ItemCreate]): + """Create multiple items concurrently (batch processing)""" + import asyncio + + # Execute all item creation tasks concurrently + tasks = [crud.create_item(item) for item in items] + created_items = await asyncio.gather(*tasks) + + return created_items + +@router.get("/batch/{item_ids}") +async def read_items_batch(item_ids: str): + """Retrieve multiple items concurrently (batch processing)""" + import asyncio + + # Parse comma-separated IDs + ids = [int(id.strip()) for id in item_ids.split(",")] + + # Execute all item retrieval tasks concurrently + tasks = [crud.get_item(item_id) for item_id in ids] + items = await asyncio.gather(*tasks) + + # Return only non-None items + return [item for item in items if item is not None] +``` + +### 批量处理测试 + +
+ +```console +# Batch creation test +$ curl -X POST "http://127.0.0.1:8000/items/batch" \ + -H "Content-Type: application/json" \ + -d '[ + {"name": "Item1", "description": "Description1", "price": 10.0, "tax": 1.0}, + {"name": "Item2", "description": "Description2", "price": 20.0, "tax": 2.0}, + {"name": "Item3", "description": "Description3", "price": 30.0, "tax": 3.0} + ]' + +# Batch retrieval test +$ curl -X GET "http://127.0.0.1:8000/items/batch/1,2,3" +``` + +
+ +## 第 7 步:进阶异步模式 + +### 实现限流 + +```python +import asyncio +from collections import defaultdict +from fastapi import HTTPException, Request +from datetime import datetime, timedelta + +class AsyncRateLimiter: + def __init__(self, max_requests: int = 100, window_seconds: int = 60): + self.max_requests = max_requests + self.window_seconds = window_seconds + self.requests = defaultdict(list) + + async def is_allowed(self, client_ip: str) -> bool: + now = datetime.now() + cutoff = now - timedelta(seconds=self.window_seconds) + + # remove old request records + self.requests[client_ip] = [ + req_time for req_time in self.requests[client_ip] + if req_time > cutoff + ] + + # check current request count + if len(self.requests[client_ip]) >= self.max_requests: + return False + + # add current request record + self.requests[client_ip].append(now) + return True + +# global rate limiter instance +rate_limiter = AsyncRateLimiter() + +@app.middleware("http") +async def rate_limit_middleware(request: Request, call_next): + client_ip = request.client.host + + if not await rate_limiter.is_allowed(client_ip): + raise HTTPException( + status_code=429, + detail="Too many requests" + ) + + response = await call_next(request) + return response +``` + +### 实现异步缓存 + +```python +import asyncio +from typing import Optional, Any +from datetime import datetime, timedelta + +class AsyncCache: + def __init__(self): + self._cache = {} + self._expiry = {} + + async def get(self, key: str) -> Optional[Any]: + # remove expired items + if key in self._expiry and datetime.now() > self._expiry[key]: + del self._cache[key] + del self._expiry[key] + return None + + return self._cache.get(key) + + async def set(self, key: str, value: Any, ttl_seconds: int = 300): + self._cache[key] = value + self._expiry[key] = datetime.now() + timedelta(seconds=ttl_seconds) + + async def delete(self, key: str): + self._cache.pop(key, None) + self._expiry.pop(key, None) + +# global cache instance +cache = AsyncCache() + +# modify CRUD methods to use cache +async def get_items_cached(self) -> List[Item]: + """Retrieve items using cache""" + cache_key = "all_items" + cached_items = await cache.get(cache_key) + + if cached_items: + return cached_items + + # if cache is not found, read from file + items = await self.get_items() + await cache.set(cache_key, items, ttl_seconds=60) # 1 minute cache + + return items +``` + +## 第 8 步:生产环境考量 + +### 管理连接池 + +```python +# add to src/core/config.py +class Settings(BaseSettings): + # ... existing settings ... + + # asynchronous processing related settings + MAX_CONCURRENT_REQUESTS: int = 100 + REQUEST_TIMEOUT: int = 30 + CONNECTION_POOL_SIZE: int = 20 + +settings = Settings() +``` + +### 改进错误处理 + +```python +import logging +from fastapi import HTTPException +from typing import Union + +logger = logging.getLogger(__name__) + +async def safe_async_operation(operation, *args, **kwargs) -> Union[Any, None]: + """Execute safe asynchronous operation""" + try: + return await operation(*args, **kwargs) + except asyncio.TimeoutError: + logger.error(f"Timeout in {operation.__name__}") + raise HTTPException(status_code=504, detail="Request timeout") + except Exception as e: + logger.error(f"Error in {operation.__name__}: {str(e)}") + raise HTTPException(status_code=500, detail="Internal server error") + +# usage example +@router.get("/safe/{item_id}") +async def read_item_safe(item_id: int): + return await safe_async_operation(crud.get_item, item_id) +``` + +## 下一步 + +恭喜您完成了异步 CRUD API 的构建!接下来可以尝试: + +1. **[数据库集成](database-integration.md)** —— 使用 PostgreSQL 与异步 SQLAlchemy +2. **[Docker 容器化](docker-deployment.md)** —— 把异步应用容器化 +3. **[自定义响应处理](custom-response-handling.md)** —— 进阶的响应格式与错误处理 + + + +## 小结 + +在本教程中,我们用异步 FastAPI 完成了: + +- ✅ 实现异步 CRUD 操作 +- ✅ 用 aiofiles 优化文件 I/O +- ✅ 处理并发请求并进行性能测试 +- ✅ 编写并运行异步测试 +- ✅ 实现批量处理与进阶异步模式 +- ✅ 处理生产环境的考量(缓存、错误处理、连接管理) + +掌握异步处理,您就能构建出高性能的 API 服务器! diff --git a/docs/zh/tutorial/basic-api-server.md b/docs/zh/tutorial/basic-api-server.md new file mode 100644 index 0000000..cb1c9a0 --- /dev/null +++ b/docs/zh/tutorial/basic-api-server.md @@ -0,0 +1,398 @@ +# 构建一个基础 API 服务器 + +学习如何使用 FastAPI-fastkit 快速构建一个简单的 REST API 服务器。本教程面向 FastAPI 初学者,覆盖基础 CRUD API 的创建。 + +## 您将学到的内容 + +- 使用 `fastkit startdemo` 命令创建基础 API 服务器 +- 理解 FastAPI 项目结构 +- 使用基础的 CRUD 端点 +- API 测试与文档 +- 项目的扩展方式 + +## 前置条件 + +- 已安装 Python 3.12 及以上 +- 已安装 FastAPI-fastkit(`pip install fastapi-fastkit`) +- 具备 Python 基础知识 + +## 第 1 步:创建基础 API 项目 + +使用 `fastapi-default` 模板创建基础 API 服务器。 + +
+ +```console +$ fastkit startdemo fastapi-default +Enter the project name: my-first-api +Enter the author name: Developer Kim +Enter the author email: developer@example.com +Enter the project description: My first FastAPI server +Deploying FastAPI project using 'fastapi-default' template + + Project Information +┌──────────────┬────────────────────────────┐ +│ Project Name │ my-first-api │ +│ Author │ Developer Kim │ +│ Author Email │ developer@example.com │ +│ Description │ My first FastAPI server │ +└──────────────┴────────────────────────────┘ + + Template Dependencies +┌──────────────┬───────────────────┐ +│ Dependency 1 │ fastapi │ +│ Dependency 2 │ uvicorn │ +│ Dependency 3 │ pydantic │ +│ Dependency 4 │ pydantic-settings │ +│ Dependency 5 │ python-dotenv │ +└──────────────┴───────────────────┘ + +Select package manager (pip, uv, pdm, poetry) [uv]: uv +Do you want to proceed with project creation? [y/N]: y + +✨ FastAPI project 'my-first-api' from 'fastapi-default' has been created successfully! +``` + +
+ +## 第 2 步:理解生成的项目结构 + +先来看看生成后的项目结构: + +``` +my-first-api/ +├── README.md # 项目文档 +├── requirements.txt # 依赖列表 +├── setup.py # 包配置 +├── scripts/ +│ └── run-server.sh # 启动服务器脚本 +├── src/ # Main source code +│ ├── main.py # FastAPI 应用入口 +│ ├── core/ +│ │ └── config.py # 配置管理 +│ ├── api/ +│ │ ├── api.py # API 路由汇总 +│ │ └── routes/ +│ │ └── items.py # items 相关端点 +│ ├── schemas/ +│ │ └── items.py # 数据模型定义 +│ ├── crud/ +│ │ └── items.py # 数据处理逻辑 +│ └── mocks/ +│ └── mock_items.json # 测试数据 +└── tests/ # 测试代码 + ├── __init__.py + ├── conftest.py + └── test_items.py +``` + +### 关键文件说明 + +- **`src/main.py`**:FastAPI 应用入口 +- **`src/api/routes/items.py`**:item 相关的 API 端点定义 +- **`src/schemas/items.py`**:请求 / 响应数据结构定义 +- **`src/crud/items.py`**:数据库操作逻辑 +- **`src/mocks/mock_items.json`**:开发用的示例数据 + +## 第 3 步:运行服务器 + +进入生成的项目目录,运行服务器。 + +
+ +```console +$ cd my-first-api +$ fastkit runserver +Starting FastAPI server at 127.0.0.1:8000... + +INFO: Will watch for changes in these directories: ['/path/to/my-first-api'] +INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) +INFO: Started reloader process [12345] using WatchFiles +INFO: Started server process [12346] +INFO: Waiting for application startup. +INFO: Application startup complete. +``` + +
+ +服务器成功运行后,您可以在浏览器中访问: + +- **API 服务器**:http://127.0.0.1:8000 +- **Swagger UI 文档**:http://127.0.0.1:8000/docs +- **ReDoc 文档**:http://127.0.0.1:8000/redoc + +## 第 4 步:探索 API 端点 + +生成的 API 默认提供以下端点: + +| 方法 | 端点 | 描述 | +|--------|----------|-------------| +| GET | `/items/` | 获取所有 item | +| GET | `/items/{item_id}` | 获取指定 item | +| POST | `/items/` | 创建新 item | +| PUT | `/items/{item_id}` | 更新 item | +| DELETE | `/items/{item_id}` | 删除 item | + +### 测试 API + +**1. 获取所有 item** + +
+ +```console +$ curl -X GET "http://127.0.0.1:8000/items/" +[ + { + "id": 1, + "name": "Laptop", + "description": "High-performance laptop", + "price": 999.99, + "tax": 99.99 + }, + { + "id": 2, + "name": "Mouse", + "description": "Wireless mouse", + "price": 29.99, + "tax": 2.99 + } +] +``` + +
+ +**2. 创建新 item** + +
+ +```console +$ curl -X POST "http://127.0.0.1:8000/items/" \ + -H "Content-Type: application/json" \ + -d '{ + "name": "Keyboard", + "description": "Mechanical keyboard", + "price": 150.00, + "tax": 15.00 + }' + +{ + "id": 3, + "name": "Keyboard", + "description": "Mechanical keyboard", + "price": 150.0, + "tax": 15.0 +} +``` + +
+ +**3. 获取指定 item** + +
+ +```console +$ curl -X GET "http://127.0.0.1:8000/items/1" +{ + "id": 1, + "name": "Laptop", + "description": "High-performance laptop", + "price": 999.99, + "tax": 99.99 +} +``` + +
+ +## 第 5 步:用 Swagger UI 测试 API + +在浏览器中访问 http://127.0.0.1:8000/docs,查看自动生成的 API 文档。 + +借助 Swagger UI 您可以: + +1. **查看 API 端点**:以可视化方式查看所有可用端点 +2. **核对请求 / 响应模式**:查看每个端点的输入输出格式 +3. **直接测试 API**:点击「Try it out」实际调用 API +4. **查看示例数据**:浏览各端点的请求 / 响应示例 + +### 如何使用 Swagger UI + +1. 点击 `/items/` GET 端点 +2. 点击「Try it out」按钮 +3. 点击「Execute」按钮 +4. 查看服务器响应 + +## 第 6 步:理解代码结构 + +### 主应用(`src/main.py`) + +```python +from fastapi import FastAPI +from src.api.api import api_router +from src.core.config import settings + +app = FastAPI( + title=settings.PROJECT_NAME, + version=settings.VERSION, + description=settings.DESCRIPTION, +) + +app.include_router(api_router) + +@app.get("/") +def read_root(): + return {"message": "Hello World"} +``` + +### Item 模式(`src/schemas/items.py`) + +```python +from pydantic import BaseModel +from typing import Optional + +class ItemBase(BaseModel): + name: str + description: Optional[str] = None + price: float + tax: Optional[float] = None + +class ItemCreate(ItemBase): + pass + +class ItemUpdate(ItemBase): + name: Optional[str] = None + price: Optional[float] = None + +class Item(ItemBase): + id: int + + class Config: + from_attributes = True +``` + +### CRUD 逻辑(`src/crud/items.py`) + +```python +from typing import List, Optional +from src.schemas.items import Item, ItemCreate, ItemUpdate + +class ItemCRUD: + def __init__(self): + self.items: List[Item] = [] + self.next_id = 1 + + def create_item(self, item: ItemCreate) -> Item: + new_item = Item(id=self.next_id, **item.dict()) + self.items.append(new_item) + self.next_id += 1 + return new_item + + def get_items(self) -> List[Item]: + return self.items + + def get_item(self, item_id: int) -> Optional[Item]: + return next((item for item in self.items if item.id == item_id), None) +``` + +## 第 7 步:扩展项目 + +### 添加新路由 + +可以使用 `fastkit addroute` 命令添加新端点: + +
+ +```console +$ fastkit addroute user + Adding New Route +┌──────────────────┬──────────────────────────────────────────┐ +│ Project │ my-first-api │ +│ Route Name │ user │ +│ Target Directory │ /path/to/my-first-api │ +└──────────────────┴──────────────────────────────────────────┘ + +Do you want to add route 'user' to the current project? [Y/n]: y + +✨ Successfully added new route 'user' to the current project! +``` + +
+ +该命令会创建以下文件: + +- `src/api/routes/user.py` —— 用户相关端点 +- `src/schemas/user.py` —— 用户数据模型 +- `src/crud/user.py` —— 用户数据处理逻辑 + +### 自定义环境配置 + +可以修改 `src/core/config.py` 文件来调整项目设置: + +```python +from pydantic_settings import BaseSettings + +class Settings(BaseSettings): + PROJECT_NAME: str = "My First API" + VERSION: str = "1.0.0" + DESCRIPTION: str = "My first FastAPI server" + API_V1_STR: str = "/api/v1" + + class Config: + env_file = ".env" + +settings = Settings() +``` + +## 第 8 步:运行测试 + +项目自带基础测试: + +
+ +```console +$ pytest tests/ -v +======================== test session starts ======================== +collected 4 items + +tests/test_items.py::test_create_item PASSED [ 25%] +tests/test_items.py::test_read_items PASSED [ 50%] +tests/test_items.py::test_read_item PASSED [ 75%] +tests/test_items.py::test_update_item PASSED [100%] + +======================== 4 passed in 0.15s ======================== +``` + +
+ +## 下一步 + +恭喜您完成了基础 API 服务器的构建!接下来可以尝试: + +1. **[构建异步 CRUD API](async-crud-api.md)** —— 学习更复杂的异步处理 +2. **[数据库集成](database-integration.md)** —— 使用 PostgreSQL 与 SQLAlchemy +3. **[Docker 容器化](docker-deployment.md)** —— 为生产部署做准备 +4. **[自定义响应处理](custom-response-handling.md)** —— 进阶的响应格式配置 + +## 故障排查 + +### 常见问题 + +**问:服务器无法启动** +答:确认虚拟环境已激活,依赖已正确安装。 + +**问:无法访问 API 端点** +答:确认服务器在正常运行,端口号(默认 8000)正确。 + +**问:API 没有出现在 Swagger UI 中** +答:确认路由器已正确包含在 `src/main.py` 中。 + +## 小结 + +在本教程中,您使用 FastAPI-fastkit 完成了: + +- ✅ 创建基础 FastAPI 项目 +- ✅ 理解项目结构 +- ✅ 使用 CRUD API 端点 +- ✅ API 文档与测试 +- ✅ 项目扩展方式 + +既然您已经掌握了 FastAPI 的基础,不妨挑战更复杂的项目! diff --git a/docs/zh/tutorial/custom-response-handling.md b/docs/zh/tutorial/custom-response-handling.md new file mode 100644 index 0000000..af5d910 --- /dev/null +++ b/docs/zh/tutorial/custom-response-handling.md @@ -0,0 +1,1393 @@ +# 自定义响应处理与进阶 API 设计 + +学习如何利用 FastAPI 的进阶特性,实现一致的响应格式、错误处理、分页与自定义 OpenAPI 文档。本节我们将通过 `fastapi-custom-response` 模板实现企业级 API 设计模式。 + +## 您将学到的内容 + +- 设计标准化的 API 响应格式 +- 全局异常处理与自定义错误响应 +- 实现分页系统 +- 过滤与排序功能 +- 自定义 OpenAPI 文档 +- API 版本管理 +- 响应缓存与优化 + +## 前置条件 + +- 完成 [Docker 容器化教程](docker-deployment.md) +- 理解 REST API 设计原则 +- 熟悉 HTTP 状态码 +- 对 OpenAPI / Swagger 有基础认识 + +## 标准化 API 响应的重要性 + +### 不一致 vs 标准化的响应 + +**有问题的响应格式:** +```json +// Success +{"id": 1, "name": "item"} + +// Error +{"detail": "Not found"} + +// List retrieval +[{"id": 1}, {"id": 2}] +``` + +**标准化的响应格式:** +```json +// Success +{ + "success": true, + "data": {"id": 1, "name": "item"}, + "message": "Item retrieved successfully", + "timestamp": "2024-01-01T12:00:00Z" +} + +// Error +{ + "success": false, + "error": { + "code": "ITEM_NOT_FOUND", + "message": "Item not found", + "details": {"item_id": 123} + }, + "timestamp": "2024-01-01T12:00:00Z" +} +``` + +## 第 1 步:创建自定义响应项目 + +使用 `fastapi-custom-response` 模板创建项目: + +
+ +```console +$ fastkit startdemo fastapi-custom-response +Enter the project name: advanced-api-server +Enter the author name: Developer Kim +Enter the author email: developer@example.com +Enter the project description: API server with advanced response handling +Deploying FastAPI project using 'fastapi-custom-response' template + + Project Information +┌──────────────┬─────────────────────────────────────────────┐ +│ Project Name │ advanced-api-server │ +│ Author │ Developer Kim │ +│ Author Email │ developer@example.com │ +│ Description │ API server with advanced response handling │ +└──────────────┴─────────────────────────────────────────────┘ + + Template Dependencies +┌──────────────┬───────────────────┐ +│ Dependency 1 │ fastapi │ +│ Dependency 2 │ uvicorn │ +│ Dependency 3 │ pydantic │ +│ Dependency 4 │ pydantic-settings │ +│ Dependency 5 │ aiofiles │ +│ Dependency 6 │ python-multipart │ +└──────────────┴───────────────────┘ + +Select package manager (pip, uv, pdm, poetry) [uv]: uv +Do you want to proceed with project creation? [y/N]: y + +✨ FastAPI project 'advanced-api-server' from 'fastapi-custom-response' has been created successfully! +``` + +
+ +## 第 2 步:分析项目结构 + +让我们看看生成项目中的进阶特性: + +``` +advanced-api-server/ +├── src/ +│ ├── main.py # FastAPI application +│ ├── schemas/ +│ │ ├── base.py # Base response schemas +│ │ ├── items.py # Item schemas +│ │ └── responses.py # Response format definitions +│ ├── helper/ +│ │ ├── exceptions.py # Custom exception classes +│ │ └── pagination.py # Pagination helpers +│ ├── utils/ +│ │ ├── responses.py # 响应工具 +│ │ └── documents.py # OpenAPI 文档定制 +│ ├── api/ +│ │ └── routes/ +│ │ └── items.py # 进阶 API 端点 +│ ├── crud/ +│ │ └── items.py # CRUD 逻辑 +│ └── core/ +│ └── config.py # 配置 +└── tests/ + └── test_responses.py # 响应格式测试 +``` + +## 第 3 步:实现标准化的响应模式 + +### 基础响应模式(`src/schemas/base.py`) + +```python +from typing import Generic, TypeVar, Optional, Any, Dict, List +from pydantic import BaseModel, Field +from datetime import datetime +from enum import Enum + +T = TypeVar('T') + +class ResponseStatus(str, Enum): + """响应状态""" + SUCCESS = "success" + ERROR = "error" + WARNING = "warning" + +class ErrorDetail(BaseModel): + """错误详情信息""" + code: str = Field(..., description="错误代码") + message: str = Field(..., description="错误信息") + field: Optional[str] = Field(None, description="发生错误的字段") + details: Optional[Dict[str, Any]] = Field(None, description="附加错误信息") + +class BaseResponse(BaseModel, Generic[T]): + """基础响应格式""" + success: bool = Field(..., description="请求是否成功") + status: ResponseStatus = Field(..., description="响应状态") + data: Optional[T] = Field(None, description="响应数据") + message: Optional[str] = Field(None, description="响应消息") + timestamp: datetime = Field(default_factory=datetime.utcnow, description="响应时间戳") + request_id: Optional[str] = Field(None, description="请求追踪 ID") + +class ErrorResponse(BaseModel): + """错误响应格式""" + success: bool = Field(False, description="请求是否成功") + status: ResponseStatus = Field(ResponseStatus.ERROR, description="响应状态") + error: ErrorDetail = Field(..., description="错误信息") + timestamp: datetime = Field(default_factory=datetime.utcnow, description="响应时间戳") + request_id: Optional[str] = Field(None, description="请求追踪 ID") + +class PaginationMeta(BaseModel): + """分页元数据""" + page: int = Field(..., ge=1, description="当前页码") + size: int = Field(..., ge=1, le=100, description="每页条数") + total: int = Field(..., ge=0, description="总条目数") + pages: int = Field(..., ge=0, description="总页数") + has_next: bool = Field(..., description="是否存在下一页") + has_prev: bool = Field(..., description="是否存在上一页") + +class PaginatedResponse(BaseModel, Generic[T]): + """分页响应格式""" + success: bool = Field(True, description="请求是否成功") + status: ResponseStatus = Field(ResponseStatus.SUCCESS, description="响应状态") + data: List[T] = Field(..., description="数据列表") + meta: PaginationMeta = Field(..., description="分页信息") + message: Optional[str] = Field(None, description="响应消息") + timestamp: datetime = Field(default_factory=datetime.utcnow, description="响应时间") + request_id: Optional[str] = Field(None, description="请求追踪 ID") + +class ValidationErrorDetail(BaseModel): + """校验错误详情""" + field: str = Field(..., description="校验失败的字段") + message: str = Field(..., description="错误信息") + invalid_value: Any = Field(..., description="无效值") + +class ValidationErrorResponse(BaseModel): + """校验错误响应格式""" + success: bool = Field(False, description="请求是否成功") + status: ResponseStatus = Field(ResponseStatus.ERROR, description="响应状态") + error: ErrorDetail = Field(..., description="错误信息") + validation_errors: List[ValidationErrorDetail] = Field(..., description="校验错误列表") + timestamp: datetime = Field(default_factory=datetime.utcnow, description="响应时间") + request_id: Optional[str] = Field(None, description="请求追踪 ID") +``` + +### 响应工具函数(`src/utils/responses.py`) + +```python +from typing import Any, Optional, List, TypeVar +from fastapi import Request +from fastapi.responses import JSONResponse +import uuid + +from src.schemas.base import ( + BaseResponse, ErrorResponse, PaginatedResponse, + ResponseStatus, ErrorDetail, PaginationMeta +) + +T = TypeVar('T') + +def generate_request_id() -> str: + """生成请求追踪 ID""" + return str(uuid.uuid4()) + +def success_response( + data: Any = None, + message: Optional[str] = None, + request_id: Optional[str] = None, + status_code: int = 200 +) -> JSONResponse: + """生成成功响应""" + response_data = BaseResponse[Any]( + success=True, + status=ResponseStatus.SUCCESS, + data=data, + message=message or "Request processed successfully", + request_id=request_id or generate_request_id() + ) + + return JSONResponse( + status_code=status_code, + content=response_data.dict(exclude_none=True) + ) + +def error_response( + error_code: str, + error_message: str, + details: Optional[dict] = None, + status_code: int = 400, + request_id: Optional[str] = None +) -> JSONResponse: + """生成错误响应""" + error_detail = ErrorDetail( + code=error_code, + message=error_message, + details=details + ) + + response_data = ErrorResponse( + error=error_detail, + request_id=request_id or generate_request_id() + ) + + return JSONResponse( + status_code=status_code, + content=response_data.dict(exclude_none=True) + ) + +def paginated_response( + data: List[T], + page: int, + size: int, + total: int, + message: Optional[str] = None, + request_id: Optional[str] = None +) -> JSONResponse: + """Generate paginated response""" + pages = (total + size - 1) // size # round up calculation + has_next = page < pages + has_prev = page > 1 + + meta = PaginationMeta( + page=page, + size=size, + total=total, + pages=pages, + has_next=has_next, + has_prev=has_prev + ) + + response_data = PaginatedResponse[T]( + data=data, + meta=meta, + message=message or f"Page {page}/{pages} data retrieved", + request_id=request_id or generate_request_id() + ) + + return JSONResponse( + status_code=200, + content=response_data.dict(exclude_none=True) + ) + +class ResponseHelper: + """Response helper class""" + + @staticmethod + def created(data: Any, message: str = "Resource created successfully") -> JSONResponse: + return success_response(data=data, message=message, status_code=201) + + @staticmethod + def updated(data: Any, message: str = "Resource updated successfully") -> JSONResponse: + return success_response(data=data, message=message, status_code=200) + + @staticmethod + def deleted(message: str = "Resource deleted successfully") -> JSONResponse: + return success_response(data=None, message=message, status_code=204) + + @staticmethod + def not_found(resource: str = "Resource") -> JSONResponse: + return error_response( + error_code="RESOURCE_NOT_FOUND", + error_message=f"{resource} not found", + status_code=404 + ) + + @staticmethod + def bad_request(message: str = "Bad request") -> JSONResponse: + return error_response( + error_code="BAD_REQUEST", + error_message=message, + status_code=400 + ) + + @staticmethod + def unauthorized(message: str = "Authentication required") -> JSONResponse: + return error_response( + error_code="UNAUTHORIZED", + error_message=message, + status_code=401 + ) + + @staticmethod + def forbidden(message: str = "Permission denied") -> JSONResponse: + return error_response( + error_code="FORBIDDEN", + error_message=message, + status_code=403 + ) + + @staticmethod + def server_error(message: str = "Server internal error occurred") -> JSONResponse: + return error_response( + error_code="INTERNAL_SERVER_ERROR", + error_message=message, + status_code=500 + ) +``` + +## 第 4 步:自定义异常处理体系 + +### 自定义异常类(`src/helper/exceptions.py`) + +```python +from typing import Optional, Dict, Any +from fastapi import HTTPException + +class BaseAPIException(HTTPException): + """Base API exception class""" + + def __init__( + self, + error_code: str, + message: str, + status_code: int = 400, + details: Optional[Dict[str, Any]] = None + ): + self.error_code = error_code + self.message = message + self.details = details or {} + super().__init__(status_code=status_code, detail=message) + +class ValidationException(BaseAPIException): + """Validation exception""" + + def __init__(self, message: str, field: Optional[str] = None, details: Optional[Dict] = None): + super().__init__( + error_code="VALIDATION_ERROR", + message=message, + status_code=422, + details=details or {"field": field} if field else None + ) + +class ResourceNotFoundException(BaseAPIException): + """Resource not found exception""" + + def __init__(self, resource: str, resource_id: Any): + super().__init__( + error_code="RESOURCE_NOT_FOUND", + message=f"{resource}(ID: {resource_id}) not found", + status_code=404, + details={"resource": resource, "id": resource_id} + ) + +class DuplicateResourceException(BaseAPIException): + """Duplicate resource exception""" + + def __init__(self, resource: str, field: str, value: Any): + super().__init__( + error_code="DUPLICATE_RESOURCE", + message=f"{resource} {field} '{value}' already exists", + status_code=409, + details={"resource": resource, "field": field, "value": value} + ) + +class BusinessLogicException(BaseAPIException): + """Business logic exception""" + + def __init__(self, message: str, error_code: str = "BUSINESS_LOGIC_ERROR"): + super().__init__( + error_code=error_code, + message=message, + status_code=422 + ) + +class RateLimitException(BaseAPIException): + """Request limit exception""" + + def __init__(self, retry_after: int = 60): + super().__init__( + error_code="RATE_LIMIT_EXCEEDED", + message="Request limit exceeded. Please try again later", + status_code=429, + details={"retry_after": retry_after} + ) + +class AuthenticationException(BaseAPIException): + """Authentication exception""" + + def __init__(self, message: str = "Authentication required"): + super().__init__( + error_code="AUTHENTICATION_REQUIRED", + message=message, + status_code=401 + ) + +class AuthorizationException(BaseAPIException): + """Authorization exception""" + + def __init__(self, message: str = "Permission denied"): + super().__init__( + error_code="INSUFFICIENT_PERMISSIONS", + message=message, + status_code=403 + ) +``` + +### 全局异常处理器(`src/main.py`) + +```python +from fastapi import FastAPI, Request, status +from fastapi.exceptions import RequestValidationError, HTTPException +from fastapi.responses import JSONResponse +from pydantic import ValidationError +import logging +import traceback + +from src.helper.exceptions import BaseAPIException +from src.utils.responses import error_response, generate_request_id +from src.schemas.base import ValidationErrorDetail, ValidationErrorResponse + +logger = logging.getLogger(__name__) + +app = FastAPI( + title="Advanced API Server", + description="API server with advanced response handling", + version="1.0.0" +) + +@app.exception_handler(BaseAPIException) +async def custom_api_exception_handler(request: Request, exc: BaseAPIException): + """Custom API exception handler""" + request_id = generate_request_id() + + logger.error( + f"API Exception: {exc.error_code} - {exc.message}", + extra={ + "request_id": request_id, + "path": request.url.path, + "method": request.method, + "details": exc.details + } + ) + + return error_response( + error_code=exc.error_code, + error_message=exc.message, + details=exc.details, + status_code=exc.status_code, + request_id=request_id + ) + +@app.exception_handler(RequestValidationError) +async def validation_exception_handler(request: Request, exc: RequestValidationError): + """Pydantic 校验异常处理器""" + request_id = generate_request_id() + + validation_errors = [] + for error in exc.errors(): + field = ".".join(str(loc) for loc in error["loc"]) + validation_errors.append( + ValidationErrorDetail( + field=field, + message=error["msg"], + invalid_value=error.get("input", "") + ) + ) + + error_response_data = ValidationErrorResponse( + error={ + "code": "VALIDATION_ERROR", + "message": "Input data validation failed", + "details": {"error_count": len(validation_errors)} + }, + validation_errors=validation_errors, + request_id=request_id + ) + + logger.warning( + f"Validation Error: {len(validation_errors)} validation errors", + extra={ + "request_id": request_id, + "path": request.url.path, + "method": request.method, + "errors": [err.dict() for err in validation_errors] + } + ) + + return JSONResponse( + status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, + content=error_response_data.dict(exclude_none=True) + ) + +@app.exception_handler(HTTPException) +async def http_exception_handler(request: Request, exc: HTTPException): + """HTTP exception handler""" + request_id = generate_request_id() + + error_code_map = { + 400: "BAD_REQUEST", + 401: "UNAUTHORIZED", + 403: "FORBIDDEN", + 404: "NOT_FOUND", + 405: "METHOD_NOT_ALLOWED", + 500: "INTERNAL_SERVER_ERROR" + } + + error_code = error_code_map.get(exc.status_code, "HTTP_ERROR") + + return error_response( + error_code=error_code, + error_message=exc.detail, + status_code=exc.status_code, + request_id=request_id + ) + +@app.exception_handler(Exception) +async def general_exception_handler(request: Request, exc: Exception): + """General exception handler""" + request_id = generate_request_id() + + logger.error( + f"Unhandled Exception: {type(exc).__name__} - {str(exc)}", + extra={ + "request_id": request_id, + "path": request.url.path, + "method": request.method, + "traceback": traceback.format_exc() + } + ) + + return error_response( + error_code="INTERNAL_SERVER_ERROR", + error_message="Unexpected error occurred", + status_code=500, + request_id=request_id + ) +``` + +## 第 5 步:进阶分页系统 + +### 分页助手(`src/helper/pagination.py`) + +```python +from typing import List, Optional, Any, Dict, Callable +from pydantic import BaseModel, Field +from fastapi import Query +from enum import Enum + +class SortOrder(str, Enum): + """Sort order""" + ASC = "asc" + DESC = "desc" + +class PaginationParams(BaseModel): + """Pagination parameters""" + page: int = Field(1, ge=1, description="Page number") + size: int = Field(20, ge=1, le=100, description="Page size") + sort_by: Optional[str] = Field(None, description="Sort field") + sort_order: SortOrder = Field(SortOrder.ASC, description="Sort order") + +class FilterParams(BaseModel): + """Filtering parameters""" + search: Optional[str] = Field(None, description="Search term") + category: Optional[str] = Field(None, description="Category") + status: Optional[str] = Field(None, description="Status") + date_from: Optional[str] = Field(None, description="Start date (YYYY-MM-DD)") + date_to: Optional[str] = Field(None, description="End date (YYYY-MM-DD)") + +def pagination_params( + page: int = Query(1, ge=1, description="Page number"), + size: int = Query(20, ge=1, le=100, description="Page size"), + sort_by: Optional[str] = Query(None, description="Sort field"), + sort_order: SortOrder = Query(SortOrder.ASC, description="Sort order") +) -> PaginationParams: + """Pagination parameters dependency""" + return PaginationParams( + page=page, + size=size, + sort_by=sort_by, + sort_order=sort_order + ) + +def filter_params( + search: Optional[str] = Query(None, description="Search term"), + category: Optional[str] = Query(None, description="Category"), + status: Optional[str] = Query(None, description="Status"), + date_from: Optional[str] = Query(None, description="Start date (YYYY-MM-DD)"), + date_to: Optional[str] = Query(None, description="End date (YYYY-MM-DD)") +) -> FilterParams: + """Filtering parameters dependency""" + return FilterParams( + search=search, + category=category, + status=status, + date_from=date_from, + date_to=date_to + ) + +class AdvancedPaginator: + """Advanced pagination class""" + + def __init__(self, data: List[Any], pagination: PaginationParams, filters: FilterParams): + self.data = data + self.pagination = pagination + self.filters = filters + self.filtered_data = self._apply_filters() + self.sorted_data = self._apply_sorting() + + def _apply_filters(self) -> List[Any]: + """Apply filters""" + filtered = self.data + + if self.filters.search: + # Filter by search term (example: search in name or description fields) + search_term = self.filters.search.lower() + filtered = [ + item for item in filtered + if (hasattr(item, 'name') and search_term in item.name.lower()) or + (hasattr(item, 'description') and item.description and search_term in item.description.lower()) + ] + + if self.filters.category: + filtered = [item for item in filtered if hasattr(item, 'category') and item.category == self.filters.category] + + if self.filters.status: + filtered = [item for item in filtered if hasattr(item, 'status') and item.status == self.filters.status] + + # Implement date filtering (if date field exists) + if self.filters.date_from or self.filters.date_to: + from datetime import datetime + filtered = self._apply_date_filter(filtered) + + return filtered + + def _apply_date_filter(self, data: List[Any]) -> List[Any]: + """Apply date filter""" + from datetime import datetime + + if not self.filters.date_from and not self.filters.date_to: + return data + + filtered = [] + for item in data: + if not hasattr(item, 'created_at'): + continue + + item_date = item.created_at.date() if hasattr(item.created_at, 'date') else item.created_at + + if self.filters.date_from: + start_date = datetime.strptime(self.filters.date_from, "%Y-%m-%d").date() + if item_date < start_date: + continue + + if self.filters.date_to: + end_date = datetime.strptime(self.filters.date_to, "%Y-%m-%d").date() + if item_date > end_date: + continue + + filtered.append(item) + + return filtered + + def _apply_sorting(self) -> List[Any]: + """Apply sorting""" + if not self.pagination.sort_by: + return self.filtered_data + + reverse = self.pagination.sort_order == SortOrder.DESC + + try: + return sorted( + self.filtered_data, + key=lambda x: getattr(x, self.pagination.sort_by, 0), + reverse=reverse + ) + except (AttributeError, TypeError): + # Return original data if sort field is not found or cannot be sorted + return self.filtered_data + + def get_page(self) -> tuple[List[Any], int]: + """Return current page data and total count""" + total = len(self.sorted_data) + start = (self.pagination.page - 1) * self.pagination.size + end = start + self.pagination.size + + page_data = self.sorted_data[start:end] + return page_data, total + + def get_metadata(self) -> Dict[str, Any]: + """Return pagination metadata""" + total = len(self.sorted_data) + pages = (total + self.pagination.size - 1) // self.pagination.size + + return { + "page": self.pagination.page, + "size": self.pagination.size, + "total": total, + "pages": pages, + "has_next": self.pagination.page < pages, + "has_prev": self.pagination.page > 1, + "filters_applied": { + "search": self.filters.search, + "category": self.filters.category, + "status": self.filters.status, + "date_range": f"{self.filters.date_from} ~ {self.filters.date_to}" if self.filters.date_from or self.filters.date_to else None + }, + "sorting": { + "field": self.pagination.sort_by, + "order": self.pagination.sort_order + } if self.pagination.sort_by else None + } +``` + +## 第 6 步:实现进阶 API 端点 + +### Item API 路由(`src/api/routes/items.py`) + +```python +from typing import List, Optional +from fastapi import APIRouter, Depends, HTTPException, Query, Path, BackgroundTasks +from fastapi.responses import JSONResponse + +from src.schemas.items import Item, ItemCreate, ItemUpdate, ItemResponse +from src.helper.pagination import pagination_params, filter_params, PaginationParams, FilterParams, AdvancedPaginator +from src.helper.exceptions import ResourceNotFoundException, DuplicateResourceException, ValidationException +from src.utils.responses import success_response, paginated_response, ResponseHelper +from src.crud.items import ItemCRUD + +router = APIRouter(prefix="/items", tags=["items"]) +crud = ItemCRUD() + +@router.post("/", response_model=dict, status_code=201) +async def create_item( + item_create: ItemCreate, + background_tasks: BackgroundTasks +) -> JSONResponse: + """ + 创建新条目 + + - **name**:条目名称(必填) + - **description**:条目描述(可选) + - **price**:价格(必填,且不小于 0) + - **category**:分类(可选) + """ + # 检查是否存在重复条目 + existing_item = await crud.get_by_name(item_create.name) + if existing_item: + raise DuplicateResourceException("Item", "name", item_create.name) + + # 业务逻辑校验 + if item_create.price < 0: + raise ValidationException("Price must be 0 or greater", "price") + + # 创建条目 + created_item = await crud.create(item_create) + + # 后台任务(如发送通知、记录日志等) + background_tasks.add_task(send_creation_notification, created_item.id) + + return ResponseHelper.created( + data=created_item.dict(), + message=f"Item '{created_item.name}' created successfully" + ) + +@router.get("/", response_model=dict) +async def list_items( + pagination: PaginationParams = Depends(pagination_params), + filters: FilterParams = Depends(filter_params) +) -> JSONResponse: + """ + 获取条目列表(支持分页、筛选与排序) + + **分页:** + - page:页码(默认:1) + - size:每页数量(默认:20,最大:100) + + **排序:** + - sort_by:排序字段(name、price、created_at 等) + - sort_order:排序顺序(asc、desc) + + **筛选:** + - search:搜索关键词(在名称或描述字段中搜索) + - category:分类筛选 + - status:状态筛选 + - date_from:开始日期(YYYY-MM-DD) + - date_to:结束日期(YYYY-MM-DD) + """ + # 获取全部条目 + all_items = await crud.get_all() + + # 应用高级分页 + paginator = AdvancedPaginator(all_items, pagination, filters) + page_data, total = paginator.get_page() + + # 在响应中加入额外元数据 + metadata = paginator.get_metadata() + + # 生成自定义提示信息 + message = f"Total {total} items, {len(page_data)} items retrieved" + if filters.search: + message += f" (Search term: '{filters.search}')" + + return paginated_response( + data=[item.dict() for item in page_data], + page=pagination.page, + size=pagination.size, + total=total, + message=message + ) + +@router.get("/search/advanced", response_model=dict) +async def advanced_search( + q: str = Query(..., min_length=1, description="搜索关键词"), + fields: List[str] = Query(["name", "description"], description="搜索字段"), + exact_match: bool = Query(False, description="是否精确匹配"), + case_sensitive: bool = Query(False, description="是否区分大小写"), + pagination: PaginationParams = Depends(pagination_params) +) -> JSONResponse: + """ + 高级搜索功能 + + - **q**:搜索关键词(必填) + - **fields**:搜索字段列表 + - **exact_match**:是否精确匹配 + - **case_sensitive**:是否区分大小写 + """ + results = await crud.advanced_search( + query=q, + fields=fields, + exact_match=exact_match, + case_sensitive=case_sensitive + ) + + # 应用分页 + total = len(results) + start = (pagination.page - 1) * pagination.size + end = start + pagination.size + page_data = results[start:end] + + return paginated_response( + data=[item.dict() for item in page_data], + page=pagination.page, + size=pagination.size, + total=total, + message=f"'{q}' search results: {total} items" + ) + +@router.get("/{item_id}", response_model=dict) +async def get_item( + item_id: int = Path(..., gt=0, description="条目 ID") +) -> JSONResponse: + """获取指定条目""" + item = await crud.get_by_id(item_id) + if not item: + raise ResourceNotFoundException("Item", item_id) + + return success_response( + data=item.dict(), + message=f"Item '{item.name}' retrieved successfully" + ) + +@router.put("/{item_id}", response_model=dict) +async def update_item( + item_id: int = Path(..., gt=0, description="条目 ID"), + item_update: ItemUpdate +) -> JSONResponse: + """更新条目""" + existing_item = await crud.get_by_id(item_id) + if not existing_item: + raise ResourceNotFoundException("Item", item_id) + + # 检查名称是否与其他条目重复 + if item_update.name and item_update.name != existing_item.name: + duplicate = await crud.get_by_name(item_update.name) + if duplicate: + raise DuplicateResourceException("Item", "name", item_update.name) + + updated_item = await crud.update(item_id, item_update) + + return ResponseHelper.updated( + data=updated_item.dict(), + message=f"Item '{updated_item.name}' updated successfully" + ) + +@router.delete("/{item_id}", response_model=dict, status_code=204) +async def delete_item( + item_id: int = Path(..., gt=0, description="条目 ID"), + force: bool = Query(False, description="是否强制删除") +) -> JSONResponse: + """删除条目""" + item = await crud.get_by_id(item_id) + if not item: + raise ResourceNotFoundException("Item", item_id) + + # 删除前校验(例如是否存在关联订单) + if not force and await crud.has_related_orders(item_id): + raise ValidationException( + "Related orders exist, cannot be deleted. Use force=true to force delete" + ) + + await crud.delete(item_id) + + return ResponseHelper.deleted( + message=f"Item '{item.name}' deleted successfully" + ) + +@router.post("/bulk", response_model=dict) +async def bulk_create_items( + items: List[ItemCreate], + skip_duplicates: bool = Query(False, description="跳过重复项") +) -> JSONResponse: + """批量创建条目""" + if len(items) > 100: + raise ValidationException("Maximum 100 items can be created at once") + + created_items = [] + skipped_items = [] + errors = [] + + for i, item_create in enumerate(items): + try: + # 检查是否存在重复项 + existing = await crud.get_by_name(item_create.name) + if existing: + if skip_duplicates: + skipped_items.append({"index": i, "name": item_create.name, "reason": "Duplicate name"}) + continue + else: + errors.append({"index": i, "name": item_create.name, "error": "Duplicate name"}) + continue + + created_item = await crud.create(item_create) + created_items.append(created_item) + + except Exception as e: + errors.append({"index": i, "name": item_create.name, "error": str(e)}) + + result = { + "created_count": len(created_items), + "skipped_count": len(skipped_items), + "error_count": len(errors), + "created_items": [item.dict() for item in created_items], + "skipped_items": skipped_items, + "errors": errors + } + + message = f"{len(created_items)} items created" + if skipped_items: + message += f", {len(skipped_items)} skipped" + if errors: + message += f", {len(errors)} errors" + + return success_response(data=result, message=message) + +async def send_creation_notification(item_id: int): + """条目创建通知(后台任务)""" + # 在实际实现中,可以通过邮件、Slack 等方式发送通知 + import asyncio + await asyncio.sleep(1) # 模拟耗时处理 + print(f"Item {item_id} creation notification sent") +``` + +## 第 7 步:自定义 OpenAPI 文档 + +### 自定义 OpenAPI 文档(`src/utils/documents.py`) + +```python +from fastapi import FastAPI +from fastapi.openapi.utils import get_openapi +from typing import Dict, Any + +def custom_openapi(app: FastAPI) -> Dict[str, Any]: + """创建自定义 OpenAPI Schema""" + if app.openapi_schema: + return app.openapi_schema + + openapi_schema = get_openapi( + title=app.title, + version=app.version, + description=app.description, + routes=app.routes, + ) + + # 添加自定义信息 + openapi_schema["info"].update({ + "contact": { + "name": "API Support", + "url": "https://example.com/support", + "email": "support@example.com" + }, + "license": { + "name": "MIT", + "url": "https://opensource.org/licenses/MIT" + }, + "termsOfService": "https://example.com/terms" + }) + + # Add server information + openapi_schema["servers"] = [ + { + "url": "https://api.example.com", + "description": "Production server" + }, + { + "url": "https://staging-api.example.com", + "description": "Staging server" + }, + { + "url": "http://localhost:8000", + "description": "Development server" + } + ] + + # Add common response schema + openapi_schema["components"]["schemas"].update({ + "SuccessResponse": { + "type": "object", + "properties": { + "success": {"type": "boolean", "example": True}, + "status": {"type": "string", "example": "success"}, + "data": {"type": "object"}, + "message": {"type": "string", "example": "Request processed successfully"}, + "timestamp": {"type": "string", "format": "date-time"}, + "request_id": {"type": "string", "example": "123e4567-e89b-12d3-a456-426614174000"} + } + }, + "ErrorResponse": { + "type": "object", + "properties": { + "success": {"type": "boolean", "example": False}, + "status": {"type": "string", "example": "error"}, + "error": { + "type": "object", + "properties": { + "code": {"type": "string", "example": "RESOURCE_NOT_FOUND"}, + "message": {"type": "string", "example": "Resource not found"}, + "details": {"type": "object"} + } + }, + "timestamp": {"type": "string", "format": "date-time"}, + "request_id": {"type": "string", "example": "123e4567-e89b-12d3-a456-426614174000"} + } + } + }) + + # Add tag grouping and description + openapi_schema["tags"] = [ + { + "name": "items", + "description": "Item management API", + "externalDocs": { + "description": "More information", + "url": "https://example.com/docs/items" + } + }, + { + "name": "health", + "description": "System status check API" + } + ] + + # Add security schema + openapi_schema["components"]["securitySchemes"] = { + "BearerAuth": { + "type": "http", + "scheme": "bearer", + "bearerFormat": "JWT" + }, + "ApiKeyAuth": { + "type": "apiKey", + "in": "header", + "name": "X-API-Key" + } + } + + app.openapi_schema = openapi_schema + return app.openapi_schema + +def setup_docs(app: FastAPI): + """Setup documentation""" + app.openapi = lambda: custom_openapi(app) + + # Swagger UI setup + app.docs_url = "/docs" + app.redoc_url = "/redoc" + + # Additional document endpoint + @app.get("/openapi.json", include_in_schema=False) + async def get_openapi_endpoint(): + return custom_openapi(app) +``` + +### 应用到主程序(`src/main.py` 中追加) + +```python +from src.utils.documents import setup_docs +from src.api.routes import items + +# Include router +app.include_router(items.router, prefix="/api/v1") + +# Apply documentation setup +setup_docs(app) + +# Add request ID middleware +@app.middleware("http") +async def add_request_id(request: Request, call_next): + request_id = generate_request_id() + request.state.request_id = request_id + + response = await call_next(request) + response.headers["X-Request-ID"] = request_id + + return response +``` + +## 第 8 步:实现缓存系统 + +### 响应缓存(`src/utils/cache.py`) + +```python +from typing import Optional, Any, Dict +from functools import wraps +import asyncio +import json +import hashlib +from datetime import datetime, timedelta + +class MemoryCache: + """Memory-based cache""" + + def __init__(self): + self._cache: Dict[str, Dict[str, Any]] = {} + + async def get(self, key: str) -> Optional[Any]: + """Get value from cache""" + if key not in self._cache: + return None + + item = self._cache[key] + if datetime.utcnow() > item["expires_at"]: + del self._cache[key] + return None + + return item["value"] + + async def set(self, key: str, value: Any, ttl_seconds: int = 300): + """Save value to cache""" + self._cache[key] = { + "value": value, + "expires_at": datetime.utcnow() + timedelta(seconds=ttl_seconds), + "created_at": datetime.utcnow() + } + + async def delete(self, key: str): + """Delete value from cache""" + self._cache.pop(key, None) + + async def clear(self): + """Delete all cache""" + self._cache.clear() + + def get_stats(self) -> Dict[str, Any]: + """Cache statistics""" + now = datetime.utcnow() + valid_items = [ + item for item in self._cache.values() + if now <= item["expires_at"] + ] + + return { + "total_items": len(self._cache), + "valid_items": len(valid_items), + "expired_items": len(self._cache) - len(valid_items), + "memory_usage_mb": len(str(self._cache)) / 1024 / 1024 + } + +# 全局缓存实例 +cache = MemoryCache() + +def cache_response(ttl_seconds: int = 300, key_prefix: str = ""): + """响应缓存装饰器""" + def decorator(func): + @wraps(func) + async def wrapper(*args, **kwargs): + # 生成缓存键 + cache_key = generate_cache_key(func.__name__, args, kwargs, key_prefix) + + # 从缓存中读取 + cached_response = await cache.get(cache_key) + if cached_response: + return cached_response + + # 执行原始函数 + response = await func(*args, **kwargs) + + # 写入缓存 + await cache.set(cache_key, response, ttl_seconds) + + return response + return wrapper + return decorator + +def generate_cache_key(func_name: str, args: tuple, kwargs: dict, prefix: str = "") -> str: + """Generate cache key""" + # Generate unique key based on function name and arguments + key_data = { + "function": func_name, + "args": str(args), + "kwargs": sorted(kwargs.items()) + } + + key_string = json.dumps(key_data, sort_keys=True) + key_hash = hashlib.md5(key_string.encode()).hexdigest() + + return f"{prefix}:{func_name}:{key_hash}" if prefix else f"{func_name}:{key_hash}" + +# Cache management endpoint +@app.get("/admin/cache/stats") +async def get_cache_stats(): + """Get cache statistics""" + stats = cache.get_stats() + return success_response(data=stats, message="Cache statistics retrieved") + +@app.delete("/admin/cache/clear") +async def clear_cache(): + """Delete all cache""" + await cache.clear() + return success_response(message="Cache deleted successfully") +``` + +### 缓存使用示例 + +```python +# Apply caching to src/api/routes/items.py + +from src.utils.cache import cache_response + +@router.get("/", response_model=dict) +@cache_response(ttl_seconds=60, key_prefix="items_list") # 1 minute caching +async def list_items( + pagination: PaginationParams = Depends(pagination_params), + filters: FilterParams = Depends(filter_params) +) -> JSONResponse: + # ... existing code ... + +@router.get("/{item_id}", response_model=dict) +@cache_response(ttl_seconds=300, key_prefix="item_detail") # 5 minute caching +async def get_item(item_id: int = Path(..., gt=0)) -> JSONResponse: + # ... existing code ... +``` + +## 第 9 步:API 测试 + +### 启动服务器并做基础测试 + +
+ +```console +$ cd advanced-api-server +$ fastkit runserver +Starting FastAPI server at 127.0.0.1:8000... + +# Custom response format test +$ curl -X POST "http://localhost:8000/api/v1/items/" \ + -H "Content-Type: application/json" \ + -d '{ + "name": "Advanced notebook", + "description": "Notebook with latest technology", + "price": 2500000, + "category": "electronics" + }' + +{ + "success": true, + "status": "success", + "data": { + "id": 1, + "name": "Advanced notebook", + "description": "Notebook with latest technology", + "price": 2500000, + "category": "electronics", + "created_at": "2024-01-01T12:00:00Z" + }, + "message": "Item 'Advanced notebook' created successfully", + "timestamp": "2024-01-01T12:00:00.123456Z", + "request_id": "123e4567-e89b-12d3-a456-426614174000" +} + +# Pagination and filtering test +$ curl "http://localhost:8000/api/v1/items/?page=1&size=10&search=notebook&sort_by=price&sort_order=desc" + +# Advanced search test +$ curl "http://localhost:8000/api/v1/items/search/advanced?q=notebook&fields=name&fields=description&exact_match=false" + +# Error response test +$ curl "http://localhost:8000/api/v1/items/999" + +{ + "success": false, + "status": "error", + "error": { + "code": "RESOURCE_NOT_FOUND", + "message": "Item (ID: 999) not found", + "details": { + "resource": "Item", + "id": 999 + } + }, + "timestamp": "2024-01-01T12:00:00.123456Z", + "request_id": "123e4567-e89b-12d3-a456-426614174000" +} +``` + +
+ +### 查看 OpenAPI 文档 + +在浏览器中访问 http://localhost:8000/docs,查看已自定义过的 API 文档。 + +## 下一步 + +恭喜您完成了自定义响应处理系统!接下来可以尝试: + +1. **[MCP 集成](mcp-integration.md)** —— 实现 Model Context Protocol + + + + +## 小结 + +在本教程中,我们实现了一套进阶的响应处理系统: + +- ✅ 设计标准化的 API 响应格式 +- ✅ 全局异常处理与自定义错误响应 +- ✅ 进阶的分页与过滤系统 +- ✅ 自定义 OpenAPI 文档 +- ✅ 响应缓存与性能优化 +- ✅ 请求追踪系统 +- ✅ 后台任务处理 +- ✅ 批量操作 API + +现在您已经能实现企业级 API 服务器的全部核心功能! diff --git a/docs/zh/tutorial/database-integration.md b/docs/zh/tutorial/database-integration.md new file mode 100644 index 0000000..1f10f09 --- /dev/null +++ b/docs/zh/tutorial/database-integration.md @@ -0,0 +1,1027 @@ +# 数据库集成(PostgreSQL + SQLAlchemy) + +使用 PostgreSQL 数据库与 SQLAlchemy ORM,构建一个可用于真实生产环境的 FastAPI 应用。本教程中我们将通过 `fastapi-psql-orm` 模板,实现完整的数据库集成。 + +## 您将学到的内容 + +- PostgreSQL 数据库的搭建与集成 +- 用 SQLAlchemy ORM 做数据建模 +- 使用 Alembic 做数据库迁移 +- 通过 Docker Compose 搭建开发环境 +- 数据库连接池管理 +- 事务处理与数据一致性 + +## 前置条件 + +- 已完成 [异步 CRUD API 教程](async-crud-api.md) +- 已安装 Docker 与 Docker Compose +- 具备 PostgreSQL 基础知识 +- 理解 SQLAlchemy ORM 的基本概念 + +## 为什么选择 PostgreSQL 与 SQLAlchemy? + +### JSON 文件 vs PostgreSQL 对比 + +| 类别 | JSON 文件 | PostgreSQL | +|----------|------------|------------| +| **性能** | 受限 | 高性能索引 | +| **并发** | 文件锁问题 | 事务支持 | +| **可扩展性** | 受内存限制 | 大规模数据处理 | +| **一致性** | 无保证 | 保证 ACID 特性 | +| **查询** | 需加载全部数据 | 支持复杂查询 | +| **备份** | 复制文件 | 完整的备份 / 恢复 | + +## 第 1 步:创建 PostgreSQL + ORM 项目 + +使用 `fastapi-psql-orm` 模板创建项目: + +
+ +```console +$ fastkit startdemo fastapi-psql-orm +Enter the project name: todo-postgres-api +Enter the author name: Developer Kim +Enter the author email: developer@example.com +Enter the project description: Todo management API using PostgreSQL +Deploying FastAPI project using 'fastapi-psql-orm' template + + Project Information +┌──────────────┬─────────────────────────────────────────┐ +│ Project Name │ todo-postgres-api │ +│ Author │ Developer Kim │ +│ Author Email │ developer@example.com │ +│ Description │ Todo management API using PostgreSQL │ +└──────────────┴─────────────────────────────────────────┘ + + Template Dependencies +┌──────────────┬────────────────┐ +│ Dependency 1 │ fastapi │ +│ Dependency 2 │ uvicorn │ +│ Dependency 3 │ sqlalchemy │ +│ Dependency 4 │ alembic │ +│ Dependency 5 │ psycopg2 │ +│ Dependency 6 │ asyncpg │ +│ Dependency 7 │ sqlmodel │ +└──────────────┴────────────────┘ + +Select package manager (pip, uv, pdm, poetry) [uv]: uv +Do you want to proceed with project creation? [y/N]: y + +✨ FastAPI project 'todo-postgres-api' from 'fastapi-psql-orm' has been created successfully! +``` + +
+ +## 第 2 步:分析项目结构 + +生成的项目提供了完整的数据库集成环境: + +``` +todo-postgres-api/ +├── docker-compose.yml # PostgreSQL container configuration +├── Dockerfile # Application container +├── alembic.ini # Alembic configuration +├── template-config.yml # Template configuration +├── scripts/ +│ ├── pre-start.sh # Pre-start initialization +│ └── test.sh # Test execution script +├── src/ +│ ├── main.py # FastAPI application +│ ├── core/ +│ │ ├── config.py # Environment configuration +│ │ └── db.py # 数据库连接配置 +│ ├── api/ +│ │ ├── deps.py # 依赖注入 +│ │ └── routes/ +│ │ └── items.py # API 端点 +│ ├── crud/ +│ │ └── items.py # 数据库操作 +│ ├── schemas/ +│ │ └── items.py # Pydantic 模型 +│ ├── utils/ +│ │ ├── backend_pre_start.py # 后端初始化 +│ │ ├── init_data.py # 初始数据加载 +│ │ └── tests_pre_start.py # 测试准备 +│ └── alembic/ +│ ├── env.py # Alembic 环境配置 +│ └── versions/ # 迁移文件 +└── tests/ + ├── conftest.py # 测试配置 + └── test_items.py # API 测试 +``` + +### 核心组件 + +1. **SQLModel**:SQLAlchemy 与 Pydantic 的融合 +2. **Alembic**:数据库模式迁移 +3. **asyncpg**:异步 PostgreSQL 驱动 +4. **Docker Compose**:开发环境容器化 + +## 第 3 步:理解数据库配置 + +### 数据库连接设置(`src/core/db.py`) + +```python +from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine +from sqlalchemy.orm import sessionmaker +from sqlmodel import SQLModel + +from src.core.config import settings + +# 创建异步 PostgreSQL 引擎 +engine = create_async_engine( + settings.DATABASE_URL, + echo=settings.DEBUG, # 输出 SQL 日志 + pool_size=20, # 连接池大小 + max_overflow=0, # 允许额外创建的连接数 + pool_pre_ping=True, # 预先检查连接状态 +) + +# 异步会话工厂 +AsyncSessionLocal = sessionmaker( + autocommit=False, + autoflush=False, + bind=engine, + class_=AsyncSession, + expire_on_commit=False, +) + +async def create_tables(): + """创建数据库表""" + async with engine.begin() as conn: + await conn.run_sync(SQLModel.metadata.create_all) + +async def get_session() -> AsyncSession: + """提供数据库会话(用于依赖注入)""" + async with AsyncSessionLocal() as session: + try: + yield session + finally: + await session.close() +``` + +### 环境配置(`src/core/config.py`) + +```python +from pydantic_settings import BaseSettings +from typing import Optional + +class Settings(BaseSettings): + PROJECT_NAME: str = "Todo PostgreSQL API" + VERSION: str = "1.0.0" + DESCRIPTION: str = "Todo management API using PostgreSQL" + + # Database configuration + POSTGRES_SERVER: str = "localhost" + POSTGRES_USER: str = "postgres" + POSTGRES_PASSWORD: str = "password" + POSTGRES_DB: str = "todoapp" + POSTGRES_PORT: int = 5432 + + # Test database + TEST_DATABASE_URL: Optional[str] = None + + # Debug mode + DEBUG: bool = False + + @property + def DATABASE_URL(self) -> str: + """Generate PostgreSQL connection URL""" + return ( + f"postgresql+asyncpg://{self.POSTGRES_USER}:" + f"{self.POSTGRES_PASSWORD}@{self.POSTGRES_SERVER}:" + f"{self.POSTGRES_PORT}/{self.POSTGRES_DB}" + ) + + class Config: + env_file = ".env" + +settings = Settings() +``` + +## 第 4 步:定义数据模型 + +### 使用 SQLModel 的数据模型(`src/schemas/items.py`) + +```python +from sqlmodel import SQLModel, Field +from typing import Optional +from datetime import datetime + +# Define common fields +class ItemBase(SQLModel): + name: str = Field(index=True, max_length=100) + description: Optional[str] = Field(default=None, max_length=500) + price: float = Field(gt=0, description="Price must be greater than 0") + tax: Optional[float] = Field(default=None, ge=0) + is_active: bool = Field(default=True) + +# Database table model +class Item(ItemBase, table=True): + __tablename__ = "items" + + id: Optional[int] = Field(default=None, primary_key=True) + created_at: datetime = Field(default_factory=datetime.utcnow) + updated_at: Optional[datetime] = Field(default=None) + + # Set index + class Config: + schema_extra = { + "example": { + "name": "notebook", + "description": "High-performance gaming notebook", + "price": 1500000.0, + "tax": 150000.0, + "is_active": True + } + } + +# API request/response model +class ItemCreate(ItemBase): + pass + +class ItemUpdate(SQLModel): + name: Optional[str] = Field(default=None, max_length=100) + description: Optional[str] = Field(default=None, max_length=500) + price: Optional[float] = Field(default=None, gt=0) + tax: Optional[float] = Field(default=None, ge=0) + is_active: Optional[bool] = Field(default=None) + +class ItemResponse(ItemBase): + id: int + created_at: datetime + updated_at: Optional[datetime] +``` + +## 第 5 步:实现 CRUD 操作 + +### 数据库 CRUD 逻辑(`src/crud/items.py`) + +```python +from typing import List, Optional +from sqlalchemy.ext.asyncio import AsyncSession +from sqlalchemy import select, update, delete +from sqlalchemy.orm import selectinload +from datetime import datetime + +from src.schemas.items import Item, ItemCreate, ItemUpdate + +class ItemCRUD: + def __init__(self, db: AsyncSession): + self.db = db + + async def create(self, item_create: ItemCreate) -> Item: + """Create new item""" + db_item = Item(**item_create.dict()) + + self.db.add(db_item) + await self.db.commit() + await self.db.refresh(db_item) + + return db_item + + async def get_by_id(self, item_id: int) -> Optional[Item]: + """Get item by ID""" + statement = select(Item).where(Item.id == item_id) + result = await self.db.execute(statement) + return result.scalar_one_or_none() + + async def get_many( + self, + skip: int = 0, + limit: int = 100, + active_only: bool = True + ) -> List[Item]: + """Get multiple items (pagination supported)""" + statement = select(Item) + + if active_only: + statement = statement.where(Item.is_active == True) + + statement = statement.offset(skip).limit(limit) + result = await self.db.execute(statement) + return result.scalars().all() + + async def update(self, item_id: int, item_update: ItemUpdate) -> Optional[Item]: + """Update item""" + # Prepare update data + update_data = item_update.dict(exclude_unset=True) + if update_data: + update_data["updated_at"] = datetime.utcnow() + + # Execute update + statement = ( + update(Item) + .where(Item.id == item_id) + .values(**update_data) + .returning(Item) + ) + + result = await self.db.execute(statement) + await self.db.commit() + + return result.scalar_one_or_none() + + async def delete(self, item_id: int) -> bool: + """Delete item (soft delete)""" + statement = ( + update(Item) + .where(Item.id == item_id) + .values(is_active=False, updated_at=datetime.utcnow()) + ) + + result = await self.db.execute(statement) + await self.db.commit() + + return result.rowcount > 0 + + async def hard_delete(self, item_id: int) -> bool: + """Delete item completely""" + statement = delete(Item).where(Item.id == item_id) + result = await self.db.execute(statement) + await self.db.commit() + + return result.rowcount > 0 + + async def search(self, query: str) -> List[Item]: + """Search item (name, description)""" + statement = select(Item).where( + (Item.name.ilike(f"%{query}%")) | + (Item.description.ilike(f"%{query}%")) + ).where(Item.is_active == True) + + result = await self.db.execute(statement) + return result.scalars().all() + + async def get_total_count(self, active_only: bool = True) -> int: + """Get total item count""" + from sqlalchemy import func + + statement = select(func.count(Item.id)) + if active_only: + statement = statement.where(Item.is_active == True) + + result = await self.db.execute(statement) + return result.scalar() +``` + +## 第 6 步:实现 API 端点 + +### 依赖注入设置(`src/api/deps.py`) + +```python +from typing import AsyncGenerator +from fastapi import Depends +from sqlalchemy.ext.asyncio import AsyncSession + +from src.core.db import get_session +from src.crud.items import ItemCRUD + +async def get_db() -> AsyncGenerator[AsyncSession, None]: + """Database session dependency""" + async for session in get_session(): + yield session + +def get_item_crud(db: AsyncSession = Depends(get_db)) -> ItemCRUD: + """Item CRUD dependency""" + return ItemCRUD(db) +``` + +### API 路由实现(`src/api/routes/items.py`) + +```python +from typing import List +from fastapi import APIRouter, Depends, HTTPException, Query, status + +from src.api.deps import get_item_crud +from src.crud.items import ItemCRUD +from src.schemas.items import Item, ItemCreate, ItemUpdate, ItemResponse + +router = APIRouter() + +@router.post("/", response_model=ItemResponse, status_code=status.HTTP_201_CREATED) +async def create_item( + item_create: ItemCreate, + crud: ItemCRUD = Depends(get_item_crud) +): + """Create new item""" + return await crud.create(item_create) + +@router.get("/", response_model=List[ItemResponse]) +async def read_items( + skip: int = Query(0, ge=0, description="Skip items"), + limit: int = Query(100, ge=1, le=1000, description="Maximum items to retrieve"), + active_only: bool = Query(True, description="Only active items"), + crud: ItemCRUD = Depends(get_item_crud) +): + """Get item list (pagination supported)""" + return await crud.get_many(skip=skip, limit=limit, active_only=active_only) + +@router.get("/search", response_model=List[ItemResponse]) +async def search_items( + q: str = Query(..., min_length=1, description="Search term"), + crud: ItemCRUD = Depends(get_item_crud) +): + """Search item""" + return await crud.search(q) + +@router.get("/count") +async def get_items_count( + active_only: bool = Query(True, description="Only active items"), + crud: ItemCRUD = Depends(get_item_crud) +): + """Get total item count""" + count = await crud.get_total_count(active_only) + return {"total": count} + +@router.get("/{item_id}", response_model=ItemResponse) +async def read_item( + item_id: int, + crud: ItemCRUD = Depends(get_item_crud) +): + """Get specific item""" + item = await crud.get_by_id(item_id) + if not item: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=f"Item ID {item_id} not found" + ) + return item + +@router.put("/{item_id}", response_model=ItemResponse) +async def update_item( + item_id: int, + item_update: ItemUpdate, + crud: ItemCRUD = Depends(get_item_crud) +): + """Update item""" + updated_item = await crud.update(item_id, item_update) + if not updated_item: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=f"Item ID {item_id} not found" + ) + return updated_item + +@router.delete("/{item_id}", status_code=status.HTTP_204_NO_CONTENT) +async def delete_item( + item_id: int, + hard_delete: bool = Query(False, description="Complete delete"), + crud: ItemCRUD = Depends(get_item_crud) +): + """Delete item""" + if hard_delete: + deleted = await crud.hard_delete(item_id) + else: + deleted = await crud.delete(item_id) + + if not deleted: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=f"Item ID {item_id} not found" + ) +``` + +## 第 7 步:运行 Docker 容器 + +### 检查 Docker Compose 配置(`docker-compose.yml`) + +```yaml +version: '3.8' + +services: + db: + image: postgres:15 + restart: always + environment: + POSTGRES_DB: todoapp + POSTGRES_USER: postgres + POSTGRES_PASSWORD: password + ports: + - "5432:5432" + volumes: + - postgres_data:/var/lib/postgresql/data + + app: + build: . + restart: always + ports: + - "8000:8000" + environment: + POSTGRES_SERVER: db + POSTGRES_USER: postgres + POSTGRES_PASSWORD: password + POSTGRES_DB: todoapp + depends_on: + - db + volumes: + - ./src:/app/src + +volumes: + postgres_data: +``` + +### 运行容器 + +
+ +```console +$ cd todo-postgres-api + +# Start service in background +$ docker-compose up -d +Creating network "todo-postgres-api_default" with the default driver +Creating volume "todo-postgres-api_postgres_data" with default driver +Pulling db (postgres:15)... +Creating todo-postgres-api_db_1 ... done +Building app +Creating todo-postgres-api_app_1 ... done + +# Check service status +$ docker-compose ps + Name Command State Ports +------------------------------------------------------------------------------------- +todo-postgres-api_app_1 uvicorn src.main:app --host=0.0.0.0 --port=8000 Up 0.0.0.0:8000->8000/tcp +todo-postgres-api_db_1 docker-entrypoint.sh postgres Up 0.0.0.0:5432->5432/tcp + +# Check log +$ docker-compose logs app +``` + +
+ +## 第 8 步:数据库迁移 + +### 使用 Alembic 创建初始迁移 + +
+ +```console +# Run migration inside container +$ docker-compose exec app alembic revision --autogenerate -m "Create items table" +INFO [alembic.runtime.migration] Context impl PostgresqlImpl. +INFO [alembic.runtime.migration] Will assume transactional DDL. +INFO [alembic.autogenerate.compare] Detected added table 'items' +Generating migration script /app/src/alembic/versions/001_create_items_table.py ... done + +# Apply migration +$ docker-compose exec app alembic upgrade head +INFO [alembic.runtime.migration] Context impl PostgresqlImpl. +INFO [alembic.runtime.migration] Will assume transactional DDL. +INFO [alembic.runtime.migration] Running upgrade -> 001, Create items table +``` + +
+ +### 查看迁移文件 + +查看生成的迁移文件: + +```python +# src/alembic/versions/001_create_items_table.py +"""Create items table + +Revision ID: 001 +Revises: +Create Date: 2024-01-01 12:00:00.000000 + +""" +from alembic import op +import sqlalchemy as sa +import sqlmodel + +# revision identifiers +revision = '001' +down_revision = None +branch_labels = None +depends_on = None + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('items', + sa.Column('name', sqlmodel.sql.sqltypes.AutoString(length=100), nullable=False), + sa.Column('description', sqlmodel.sql.sqltypes.AutoString(length=500), nullable=True), + sa.Column('price', sa.Float(), nullable=False), + sa.Column('tax', sa.Float(), nullable=True), + sa.Column('is_active', sa.Boolean(), nullable=False), + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('created_at', sa.DateTime(), nullable=False), + sa.Column('updated_at', sa.DateTime(), nullable=True), + sa.PrimaryKeyConstraint('id') + ) + op.create_index(op.f('ix_items_name'), 'items', ['name'], unique=False) + # ### end Alembic commands ### + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_index(op.f('ix_items_name'), table_name='items') + op.drop_table('items') + # ### end Alembic commands ### +``` + +## 第 9 步:API 测试 + +### 基础 CRUD 测试 + +
+ +```console +# Create new item +$ curl -X POST "http://localhost:8000/items/" \ + -H "Content-Type: application/json" \ + -d '{ + "name": "MacBook Pro", + "description": "M2 chipset-equipped high-performance notebook", + "price": 2500000, + "tax": 250000 + }' + +{ + "id": 1, + "name": "MacBook Pro", + "description": "M2 chipset-equipped high-performance notebook", + "price": 2500000.0, + "tax": 250000.0, + "is_active": true, + "created_at": "2024-01-01T12:00:00.123456", + "updated_at": null +} + +# Get item list +$ curl "http://localhost:8000/items/" + +# Get item list with pagination +$ curl "http://localhost:8000/items/?skip=0&limit=10" + +# Search item +$ curl "http://localhost:8000/items/search?q=MacBook" + +# Get item count +$ curl "http://localhost:8000/items/count" +{"total": 1} +``` + +
+ +### 进阶查询能力测试 + +
+ +```console +# Get item list with inactive items +$ curl "http://localhost:8000/items/?active_only=false" + +# Update item +$ curl -X PUT "http://localhost:8000/items/1" \ + -H "Content-Type: application/json" \ + -d '{ + "price": 2300000, + "tax": 230000 + }' + +# Soft delete item +$ curl -X DELETE "http://localhost:8000/items/1" + +# Hard delete item +$ curl -X DELETE "http://localhost:8000/items/1?hard_delete=true" +``` + +
+ +## 第 10 步:进阶数据库特性 + +### 事务处理 + +```python +# Add to src/crud/items.py + +from sqlalchemy.exc import SQLAlchemyError + +async def create_items_batch(self, items_create: List[ItemCreate]) -> List[Item]: + """Create multiple items in a transaction""" + created_items = [] + + try: + for item_create in items_create: + db_item = Item(**item_create.dict()) + self.db.add(db_item) + created_items.append(db_item) + + await self.db.commit() + + # Refresh all items + for item in created_items: + await self.db.refresh(item) + + return created_items + + except SQLAlchemyError: + await self.db.rollback() + raise +``` + +### 关系型数据建模 + +```python +# Add to src/schemas/items.py + +from sqlmodel import Relationship + +class Category(SQLModel, table=True): + __tablename__ = "categories" + + id: Optional[int] = Field(default=None, primary_key=True) + name: str = Field(max_length=50, unique=True) + description: Optional[str] = None + + # Set relationship + items: List["Item"] = Relationship(back_populates="category") + +class Item(ItemBase, table=True): + __tablename__ = "items" + + id: Optional[int] = Field(default=None, primary_key=True) + created_at: datetime = Field(default_factory=datetime.utcnow) + updated_at: Optional[datetime] = Field(default=None) + + # Add foreign key + category_id: Optional[int] = Field(foreign_key="categories.id") + + # Set relationship + category: Optional[Category] = Relationship(back_populates="items") +``` + +### 索引优化 + +```python +# Add to src/schemas/items.py + +from sqlalchemy import Index + +class Item(ItemBase, table=True): + __tablename__ = "items" + + # ... existing fields ... + + # Set composite index + __table_args__ = ( + Index('ix_items_price_active', 'price', 'is_active'), + Index('ix_items_created_at', 'created_at'), + Index('ix_items_name_description', 'name', 'description'), # For full text search + ) +``` + +## 第 11 步:编写测试 + +### 数据库测试配置(`tests/conftest.py`) + +```python +import pytest +import asyncio +from httpx import AsyncClient +from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine +from sqlalchemy.orm import sessionmaker +from sqlmodel import SQLModel + +from src.main import app +from src.core.db import get_session +from src.core.config import settings + +# Test database engine +test_engine = create_async_engine( + settings.TEST_DATABASE_URL or "sqlite+aiosqlite:///./test.db", + echo=False, +) + +TestSessionLocal = sessionmaker( + autocommit=False, + autoflush=False, + bind=test_engine, + class_=AsyncSession, + expire_on_commit=False, +) + +@pytest.fixture(scope="session") +def event_loop(): + loop = asyncio.get_event_loop_policy().new_event_loop() + yield loop + loop.close() + +@pytest.fixture(scope="function") +async def db_session(): + # Create test table + async with test_engine.begin() as conn: + await conn.run_sync(SQLModel.metadata.create_all) + + # Provide session + async with TestSessionLocal() as session: + yield session + + # Delete table after test + async with test_engine.begin() as conn: + await conn.run_sync(SQLModel.metadata.drop_all) + +@pytest.fixture +async def client(db_session: AsyncSession): + # Override dependency + async def override_get_session(): + yield db_session + + app.dependency_overrides[get_session] = override_get_session + + async with AsyncClient(app=app, base_url="http://test") as client: + yield client + + app.dependency_overrides.clear() +``` + +### 集成测试(`tests/test_items.py`) + +```python +import pytest +from httpx import AsyncClient + +@pytest.mark.asyncio +async def test_create_and_read_item(client: AsyncClient): + """Integration test for creating and reading item""" + # Create item + item_data = { + "name": "Test Item", + "description": "Database test", + "price": 50000, + "tax": 5000 + } + + response = await client.post("/items/", json=item_data) + assert response.status_code == 201 + + created_item = response.json() + assert created_item["name"] == item_data["name"] + assert "id" in created_item + assert "created_at" in created_item + + # Get created item + item_id = created_item["id"] + response = await client.get(f"/items/{item_id}") + assert response.status_code == 200 + + retrieved_item = response.json() + assert retrieved_item["id"] == item_id + assert retrieved_item["name"] == item_data["name"] + +@pytest.mark.asyncio +async def test_item_pagination(client: AsyncClient): + """Test pagination feature""" + # Create multiple items + for i in range(15): + item_data = { + "name": f"Item {i}", + "description": f"Description {i}", + "price": i * 1000, + "tax": i * 100 + } + await client.post("/items/", json=item_data) + + # Get first page + response = await client.get("/items/?skip=0&limit=10") + assert response.status_code == 200 + + items = response.json() + assert len(items) == 10 + + # Get second page + response = await client.get("/items/?skip=10&limit=10") + assert response.status_code == 200 + + items = response.json() + assert len(items) == 5 + +@pytest.mark.asyncio +async def test_item_search(client: AsyncClient): + """Test search feature""" + # Create test items + items = [ + {"name": "iPhone 15", "description": "Latest smartphone", "price": 1200000, "tax": 120000}, + {"name": "Galaxy S24", "description": "Samsung flagship", "price": 1100000, "tax": 110000}, + {"name": "MacBook Air", "description": "Apple notebook", "price": 1500000, "tax": 150000}, + ] + + for item in items: + await client.post("/items/", json=item) + + # Search "iPhone" + response = await client.get("/items/search?q=iPhone") + assert response.status_code == 200 + + results = response.json() + assert len(results) == 1 + assert results[0]["name"] == "iPhone 15" + + # Search "smartphone" (description) + response = await client.get("/items/search?q=smartphone") + assert response.status_code == 200 + + results = response.json() + assert len(results) == 1 + assert results[0]["description"] == "Latest smartphone" +``` + +### 运行测试 + +
+ +```console +# Run tests inside container +$ docker-compose exec app python -m pytest tests/ -v +======================== test session starts ======================== +collected 12 items + +tests/test_items.py::test_create_and_read_item PASSED [ 8%] +tests/test_items.py::test_item_pagination PASSED [16%] +tests/test_items.py::test_item_search PASSED [25%] +tests/test_items.py::test_update_item PASSED [33%] +tests/test_items.py::test_delete_item PASSED [41%] +tests/test_items.py::test_soft_delete PASSED [50%] +tests/test_items.py::test_item_not_found PASSED [58%] +tests/test_items.py::test_invalid_item_data PASSED [66%] +tests/test_items.py::test_database_transaction PASSED [75%] +tests/test_items.py::test_concurrent_operations PASSED [83%] +tests/test_items.py::test_item_count PASSED [91%] +tests/test_items.py::test_batch_operations PASSED [100%] + +======================== 12 passed in 2.34s ======================== +``` + +
+ +## 第 12 步:生产部署的考量 + +### 优化连接池 + +```python +# Add to src/core/config.py + +class Settings(BaseSettings): + # ... existing settings ... + + # Database connection pool settings + DB_POOL_SIZE: int = 20 + DB_MAX_OVERFLOW: int = 0 + DB_POOL_PRE_PING: bool = True + DB_POOL_RECYCLE: int = 300 # 5 minutes + + # Query timeout + DB_QUERY_TIMEOUT: int = 30 + + # Connection retry settings + DB_RETRY_ATTEMPTS: int = 3 + DB_RETRY_DELAY: int = 1 +``` + +### 数据库监控 + +```python +# Add to src/core/db.py + +import logging +from sqlalchemy import event +from sqlalchemy.engine import Engine + +logger = logging.getLogger(__name__) + +@event.listens_for(Engine, "before_cursor_execute") +def receive_before_cursor_execute(conn, cursor, statement, parameters, context, executemany): + """Log before query execution""" + context._query_start_time = time.time() + +@event.listens_for(Engine, "after_cursor_execute") +def receive_after_cursor_execute(conn, cursor, statement, parameters, context, executemany): + """Log after query execution""" + total = time.time() - context._query_start_time + if total > 1.0: # Log slow queries (1 second or more) + logger.warning(f"Slow query: {total:.2f}s - {statement[:100]}...") +``` + +## 下一步 + +恭喜您完成了 PostgreSQL 数据库集成!接下来可以尝试: + +1. **[Docker 容器化](docker-deployment.md)** —— 构建生产部署环境 +2. **[自定义响应处理](custom-response-handling.md)** —— 进阶的 API 响应格式 + + + +## 小结 + +在本教程中,我们用 PostgreSQL 与 SQLAlchemy 完成了: + +- ✅ 集成 PostgreSQL 数据库 +- ✅ 使用 SQLModel 实现 ORM +- ✅ 配置 Alembic 迁移系统 +- ✅ 进阶的 CRUD 操作与查询优化 +- ✅ 事务处理与数据一致性 +- ✅ 分页、搜索与排序功能 +- ✅ 集成测试与数据库测试 +- ✅ 生产部署的考量 + +现在您可以构建出能在真实生产环境中使用的、稳健的、数据库驱动型 API! diff --git a/docs/zh/tutorial/docker-deployment.md b/docs/zh/tutorial/docker-deployment.md new file mode 100644 index 0000000..7ea5d0a --- /dev/null +++ b/docs/zh/tutorial/docker-deployment.md @@ -0,0 +1,1177 @@ +# Docker 容器化与部署 + +学习如何用 Docker 把 FastAPI 应用容器化,构建一致的开发环境并为生产部署做准备。本节我们将通过 `fastapi-dockerized` 模板搭建一套完整的 Docker 部署环境。 + +## 您将学到的内容 + +- 用 Docker 把 FastAPI 应用容器化 +- 通过多阶段构建生成优化的 Docker 镜像 +- 用 Docker Compose 搭建开发环境 +- 面向生产部署的 Docker 配置 +- 容器监控与日志管理 +- 构建 CI/CD 流水线 + +## 前置条件 + +- 完成 [数据库集成教程](database-integration.md) +- 已安装 Docker 与 Docker Compose +- 熟悉常见的 Docker 命令 +- 对容器概念有基础认知 + +## Docker 容器化的优势 + +### 传统方式 vs Docker 方式 + +| 类别 | 传统方式 | Docker 方式 | +|----------|---------------------|-----------------| +| **环境一致性** | 各环境间存在差异 | 各处环境一致 | +| **依赖管理** | 需要手动安装 | 所有依赖打入镜像 | +| **部署速度** | 慢 | 可快速部署 | +| **可扩展性** | 受限 | 易于扩展 | +| **回滚** | 复杂 | 可立即回滚到上一版本 | +| **资源占用** | 较重 | 轻量级容器 | + +## 第 1 步:创建基于 Docker 的项目 + +使用 `fastapi-dockerized` 模板创建项目: + +
+ +```console +$ fastkit startdemo fastapi-dockerized +Enter the project name: dockerized-todo-api +Enter the author name: Developer Kim +Enter the author email: developer@example.com +Enter the project description: Dockerized todo management API +Deploying FastAPI project using 'fastapi-dockerized' template + + Project Information +┌──────────────┬─────────────────────────────────────────────┐ +│ Project Name │ dockerized-todo-api │ +│ Author │ Developer Kim │ +│ Author Email │ developer@example.com │ +│ Description │ Dockerized todo management API │ +└──────────────┴─────────────────────────────────────────────┘ + + Template Dependencies +┌──────────────┬───────────────────┐ +│ Dependency 1 │ fastapi │ +│ Dependency 2 │ uvicorn │ +│ Dependency 3 │ pydantic │ +│ Dependency 4 │ pydantic-settings │ +│ Dependency 5 │ python-dotenv │ +└──────────────┴───────────────────┘ + +Select package manager (pip, uv, pdm, poetry) [uv]: uv +Do you want to proceed with project creation? [y/N]: y + +✨ FastAPI project 'dockerized-todo-api' from 'fastapi-dockerized' has been created successfully! +``` + +
+ +## 第 2 步:分析 Docker 配置文件 + +让我们看看生成项目中的 Docker 相关文件: + +``` +dockerized-todo-api/ +├── Dockerfile # Docker image build configuration +├── docker-compose.yml # Development environment container setup +├── docker-compose.prod.yml # Production environment configuration +├── .dockerignore # Files to exclude during Docker build +├── scripts/ +│ ├── start.sh # Container startup script +│ ├── prestart.sh # Pre-start initialization script +│ └── gunicorn.conf.py # Gunicorn configuration +├── src/ +│ ├── main.py # FastAPI application +│ └── ... # Other source code +└── requirements.txt # Python 依赖 +``` + +### Dockerfile 解析 + +```dockerfile +# 使用多阶段构建优化 Dockerfile + +# ============================================ +# 阶段 1:构建阶段 +# ============================================ +FROM python:3.12-slim as builder + +# 安装构建工具 +RUN apt-get update && apt-get install -y \ + build-essential \ + curl \ + && rm -rf /var/lib/apt/lists/* + +# 复制依赖文件并安装 +COPY requirements.txt . +RUN pip install --user --no-cache-dir -r requirements.txt + +# ============================================ +# 阶段 2:运行阶段 +# ============================================ +FROM python:3.12-slim + +# 更新系统并安装必要软件包 +RUN apt-get update && apt-get install -y \ + curl \ + && rm -rf /var/lib/apt/lists/* \ + && apt-get clean + +# 创建非 root 用户(增强安全性) +RUN groupadd -r appuser && useradd -r -g appuser appuser + +# 创建应用目录 +WORKDIR /app + +# 从构建阶段复制 Python 包 +COPY --from=builder /root/.local /home/appuser/.local + +# 复制应用代码 +COPY . . + +# 设置文件权限 +RUN chown -R appuser:appuser /app +RUN chmod +x scripts/start.sh scripts/prestart.sh + +# 将 Python 包路径加入 PATH +ENV PATH=/home/appuser/.local/bin:$PATH + +# 切换到非 root 用户 +USER appuser + +# 配置健康检查 +HEALTHCHECK --interval=30s --timeout=30s --start-period=5s --retries=3 \ + CMD curl -f http://localhost:8000/health || exit 1 + +# 暴露端口 +EXPOSE 8000 + +# 执行启动脚本 +CMD ["./scripts/start.sh"] +``` + +### Docker Compose 开发环境(`docker-compose.yml`) + +```yaml +version: '3.8' + +services: + app: + build: + context: . + dockerfile: Dockerfile + container_name: dockerized-todo-api + restart: unless-stopped + ports: + - "8000:8000" + environment: + - ENVIRONMENT=development + - DEBUG=true + - RELOAD=true + volumes: + # 挂载开发用卷(代码变化时自动重载) + - ./src:/app/src:ro + - ./scripts:/app/scripts:ro + networks: + - app-network + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:8000/health"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 40s + + # Redis (for caching and session store) + redis: + image: redis:7-alpine + container_name: dockerized-todo-redis + restart: unless-stopped + ports: + - "6379:6379" + volumes: + - redis_data:/data + networks: + - app-network + healthcheck: + test: ["CMD", "redis-cli", "ping"] + interval: 30s + timeout: 10s + retries: 3 + + # Nginx (reverse proxy) + nginx: + image: nginx:alpine + container_name: dockerized-todo-nginx + restart: unless-stopped + ports: + - "80:80" + - "443:443" + volumes: + - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro + - ./nginx/ssl:/etc/nginx/ssl:ro + depends_on: + - app + networks: + - app-network + healthcheck: + test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://localhost/health"] + interval: 30s + timeout: 10s + retries: 3 + +volumes: + redis_data: + +networks: + app-network: + driver: bridge +``` + +### Docker Compose 生产环境(`docker-compose.prod.yml`) + +```yaml +version: '3.8' + +services: + app: + build: + context: . + dockerfile: Dockerfile + restart: always + environment: + - ENVIRONMENT=production + - DEBUG=false + - WORKERS=4 + - MAX_WORKERS=8 + volumes: + - app_logs:/app/logs + networks: + - app-network + deploy: + replicas: 2 + resources: + limits: + cpus: '1.0' + memory: 1G + reservations: + cpus: '0.5' + memory: 512M + restart_policy: + condition: on-failure + delay: 5s + max_attempts: 3 + + redis: + image: redis:7-alpine + restart: always + command: redis-server --appendonly yes --requirepass ${REDIS_PASSWORD} + volumes: + - redis_data:/data + networks: + - app-network + deploy: + resources: + limits: + cpus: '0.5' + memory: 512M + + nginx: + image: nginx:alpine + restart: always + ports: + - "80:80" + - "443:443" + volumes: + - ./nginx/nginx.prod.conf:/etc/nginx/nginx.conf:ro + - ./nginx/ssl:/etc/nginx/ssl:ro + - nginx_logs:/var/log/nginx + depends_on: + - app + networks: + - app-network + deploy: + resources: + limits: + cpus: '0.5' + memory: 256M + +volumes: + redis_data: + app_logs: + nginx_logs: + +networks: + app-network: + driver: overlay + attachable: true +``` + +## 第 3 步:配置启动脚本 + +### 主启动脚本(`scripts/start.sh`) + +```bash +#!/bin/bash + +set -e + +# 设置环境变量 +export PYTHONPATH=/app:$PYTHONPATH + +# 运行预启动脚本 +echo "Running pre-start script..." +./scripts/prestart.sh + +# 根据环境决定运行模式 +if [[ "$ENVIRONMENT" == "production" ]]; then + echo "Starting production server with Gunicorn..." + exec gunicorn src.main:app \ + --config scripts/gunicorn.conf.py \ + --bind 0.0.0.0:8000 \ + --workers ${WORKERS:-4} \ + --worker-class uvicorn.workers.UvicornWorker \ + --max-requests 1000 \ + --max-requests-jitter 100 \ + --preload \ + --access-logfile - \ + --error-logfile - +else + echo "Starting development server with Uvicorn..." + if [[ "$RELOAD" == "true" ]]; then + exec uvicorn src.main:app \ + --host 0.0.0.0 \ + --port 8000 \ + --reload \ + --reload-dir src \ + --log-level debug + else + exec uvicorn src.main:app \ + --host 0.0.0.0 \ + --port 8000 \ + --log-level info + fi +fi +``` + +### 预启动脚本(`scripts/prestart.sh`) + +```bash +#!/bin/bash + +set -e + +echo "Running pre-start checks..." + +# 检查 Python 模块和依赖 +echo "Checking Python dependencies..." +python -c "import fastapi, uvicorn, pydantic; print('✓ Core dependencies OK')" + +# 检查环境变量 +if [[ -z "$ENVIRONMENT" ]]; then + export ENVIRONMENT="development" + echo "ℹ ENVIRONMENT not set, defaulting to development" +fi + +# 创建日志目录 +mkdir -p /app/logs +touch /app/logs/app.log + +# 检查是否存在 health 端点 +echo "Checking health endpoint..." +python -c " +from src.main import app +routes = [route.path for route in app.routes] +if '/health' not in routes: + print('⚠ Warning: /health endpoint not found') +else: + print('✓ Health endpoint OK') +" + +echo "Pre-start checks completed successfully!" +``` + +### Gunicorn 配置(`scripts/gunicorn.conf.py`) + +```python +import multiprocessing +import os + +# 服务端监听配置 +bind = "0.0.0.0:8000" +backlog = 2048 + +# Worker 进程配置 +workers = int(os.getenv("WORKERS", multiprocessing.cpu_count() * 2 + 1)) +worker_class = "uvicorn.workers.UvicornWorker" +worker_connections = 1000 +max_requests = 1000 +max_requests_jitter = 100 + +# Worker 重启设置 +preload_app = True +timeout = 120 +keepalive = 2 + +# 日志配置 +accesslog = "-" +errorlog = "-" +loglevel = "info" +access_log_format = '%(h)s %(l)s %(u)s %(t)s "%(r)s" %(s)s %(b)s "%(f)s" "%(a)s" %(D)s' + +# 进程名称 +proc_name = "dockerized-todo-api" + +# 安全限制 +limit_request_line = 4094 +limit_request_fields = 100 +limit_request_field_size = 8190 + +# 性能调优 +def when_ready(server): + server.log.info("Server is ready. Spawning workers") + +def worker_int(worker): + worker.log.info("worker received INT or QUIT signal") + +def pre_fork(server, worker): + server.log.info("Worker spawned (pid: %s)", worker.pid) + +def post_fork(server, worker): + server.log.info("Worker spawned (pid: %s)", worker.pid) + +def worker_abort(worker): + worker.log.info("worker received SIGABRT signal") +``` + +## 第 4 步:实现健康检查与监控 + +### 添加健康检查端点(`src/main.py`) + +```python +from fastapi import FastAPI, status, Depends +from fastapi.responses import JSONResponse +import psutil +import time +from datetime import datetime + +app = FastAPI( + title="Dockerized Todo API", + description="Dockerized todo management API", + version="1.0.0" +) + +# 应用启动时间 +start_time = time.time() + +@app.get("/health", status_code=status.HTTP_200_OK) +async def health_check(): + """ + 容器健康检查端点 + """ + current_time = time.time() + uptime = current_time - start_time + + # 系统资源信息 + memory_info = psutil.virtual_memory() + cpu_percent = psutil.cpu_percent(interval=1) + + health_data = { + "status": "healthy", + "timestamp": datetime.utcnow().isoformat(), + "uptime_seconds": round(uptime, 2), + "version": app.version, + "system": { + "memory_usage_percent": memory_info.percent, + "memory_available_mb": round(memory_info.available / 1024 / 1024, 2), + "cpu_usage_percent": cpu_percent, + }, + "checks": { + "database": await check_database_connection(), + "redis": await check_redis_connection(), + "disk_space": check_disk_space(), + } + } + + # 检查所有健康项是否通过 + all_checks_passed = all(health_data["checks"].values()) + + if not all_checks_passed: + return JSONResponse( + status_code=status.HTTP_503_SERVICE_UNAVAILABLE, + content=health_data + ) + + return health_data + +async def check_database_connection() -> bool: + """检查数据库连接状态""" + try: + # 在实际实现中,这里应执行数据库连通性检测 + return True + except Exception: + return False + +async def check_redis_connection() -> bool: + """检查 Redis 连接状态""" + try: + # 在实际实现中,这里应执行 Redis 连通性检测 + return True + except Exception: + return False + +def check_disk_space() -> bool: + """检查磁盘空间""" + disk_usage = psutil.disk_usage('/') + free_percentage = (disk_usage.free / disk_usage.total) * 100 + return free_percentage > 10 # 至少保留 10% 可用空间 + +@app.get("/health/ready", status_code=status.HTTP_200_OK) +async def readiness_check(): + """ + Kubernetes 就绪探针端点 + """ + # 检查应用是否已准备好接收流量 + return {"status": "ready", "timestamp": datetime.utcnow().isoformat()} + +@app.get("/health/live", status_code=status.HTTP_200_OK) +async def liveness_check(): + """ + Kubernetes 存活探针端点 + """ + return {"status": "alive", "timestamp": datetime.utcnow().isoformat()} +``` + +## 第 5 步:配置 Nginx 反向代理 + +### 开发环境 Nginx 配置(`nginx/nginx.conf`) + +```nginx +events { + worker_connections 1024; +} + +http { + upstream fastapi_backend { + # 通过容器名指定后端服务 + server app:8000; + } + + # 定义日志格式 + log_format main '$remote_addr - $remote_user [$time_local] "$request" ' + '$status $body_bytes_sent "$http_referer" ' + '"$http_user_agent" "$http_x_forwarded_for" ' + 'rt=$request_time uct="$upstream_connect_time" ' + 'uht="$upstream_header_time" urt="$upstream_response_time"'; + + access_log /var/log/nginx/access.log main; + error_log /var/log/nginx/error.log warn; + + # 默认设置 + sendfile on; + tcp_nopush on; + tcp_nodelay on; + keepalive_timeout 65; + types_hash_max_size 2048; + client_max_body_size 100M; + + # Gzip 压缩 + gzip on; + gzip_vary on; + gzip_min_length 1024; + gzip_types text/plain text/css text/xml text/javascript + application/json application/javascript application/xml+rss + application/atom+xml image/svg+xml; + + server { + listen 80; + server_name localhost; + + # 安全响应头 + add_header X-Content-Type-Options nosniff; + add_header X-Frame-Options DENY; + add_header X-XSS-Protection "1; mode=block"; + + # 健康检查端点 + location /health { + proxy_pass http://fastapi_backend; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + + # 健康检查应快速返回 + proxy_connect_timeout 5s; + proxy_send_timeout 5s; + proxy_read_timeout 5s; + } + + # API 入口 + location / { + proxy_pass http://fastapi_backend; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + + # 超时设置 + proxy_connect_timeout 30s; + proxy_send_timeout 30s; + proxy_read_timeout 30s; + + # 缓冲设置 + proxy_buffering on; + proxy_buffer_size 4k; + proxy_buffers 8 4k; + } + + # 静态文件缓存(后续可扩展) + location /static { + expires 1y; + add_header Cache-Control public; + add_header ETag ""; + } + } +} +``` + +### 生产环境 Nginx 配置(`nginx/nginx.prod.conf`) + +```nginx +events { + worker_connections 2048; +} + +http { + upstream fastapi_backend { + # Load balancing for multiple app instances + server app:8000 max_fails=3 fail_timeout=30s; + # server app2:8000 max_fails=3 fail_timeout=30s; # For scaling + + # Keep-alive + keepalive 32; + } + + # Security settings + server_tokens off; + + # Rate limiting + limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s; + limit_req_zone $binary_remote_addr zone=health:10m rate=100r/s; + + # SSL settings + ssl_protocols TLSv1.2 TLSv1.3; + ssl_ciphers ECDHE-RSA-AES256-GCM-SHA512:DHE-RSA-AES256-GCM-SHA512:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES256-GCM-SHA384; + ssl_prefer_server_ciphers off; + ssl_session_cache shared:SSL:10m; + ssl_session_timeout 10m; + + server { + listen 80; + server_name your-domain.com; + return 301 https://$server_name$request_uri; + } + + server { + listen 443 ssl http2; + server_name your-domain.com; + + ssl_certificate /etc/nginx/ssl/cert.pem; + ssl_certificate_key /etc/nginx/ssl/key.pem; + + # Security headers + add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; + add_header X-Content-Type-Options nosniff always; + add_header X-Frame-Options DENY always; + add_header X-XSS-Protection "1; mode=block" always; + add_header Referrer-Policy "strict-origin-when-cross-origin" always; + + # Health check (rate limit applied) + location /health { + limit_req zone=health burst=20 nodelay; + proxy_pass http://fastapi_backend; + include /etc/nginx/proxy_params; + } + + # API endpoint (rate limit applied) + location / { + limit_req zone=api burst=20 nodelay; + proxy_pass http://fastapi_backend; + include /etc/nginx/proxy_params; + } + } +} +``` + +## 第 6 步:构建并运行容器 + +### 在开发环境运行 + +
+ +```console +$ cd dockerized-todo-api + +# Build Docker image +$ docker-compose build +Building app +Step 1/15 : FROM python:3.12-slim as builder + ---> abc123def456 +Step 2/15 : RUN apt-get update && apt-get install -y build-essential curl + ---> Running in xyz789abc123 +... +Successfully built def456ghi789 +Successfully tagged dockerized-todo-api_app:latest + +# Run container (background) +$ docker-compose up -d +Creating network "dockerized-todo-api_app-network" with driver "bridge" +Creating volume "dockerized-todo-api_redis_data" with default driver +Creating dockerized-todo-redis ... done +Creating dockerized-todo-api ... done +Creating dockerized-todo-nginx ... done + +# Check container status +$ docker-compose ps + Name Command State Ports +------------------------------------------------------------------------------------------------ +dockerized-todo-api ./scripts/start.sh Up (healthy) 8000/tcp +dockerized-todo-nginx /docker-entrypoint.sh ngin ... Up 0.0.0.0:80->80/tcp, :::80->80/tcp +dockerized-todo-redis docker-entrypoint.sh redis ... Up (healthy) 0.0.0.0:6379->6379/tcp, :::6379->6379/tcp +``` + +
+ +### 查看日志 + +
+ +```console +# Check all service logs +$ docker-compose logs + +# Check specific service logs +$ docker-compose logs app +$ docker-compose logs nginx +$ docker-compose logs redis + +# Check real-time logs +$ docker-compose logs -f app +``` + +
+ +### 健康检查测试 + +
+ +```console +# Basic health check +$ curl http://localhost/health +{ + "status": "healthy", + "timestamp": "2024-01-01T12:00:00.123456", + "uptime_seconds": 45.67, + "version": "1.0.0", + "system": { + "memory_usage_percent": 25.3, + "memory_available_mb": 3072.45, + "cpu_usage_percent": 5.2 + }, + "checks": { + "database": true, + "redis": true, + "disk_space": true + } +} + +# Kubernetes probe test +$ curl http://localhost/health/ready +$ curl http://localhost/health/live +``` + +
+ +## 第 7 步:生产部署 + +### 设置环境变量(`.env.prod`) + +```bash +# Application settings +ENVIRONMENT=production +DEBUG=false +SECRET_KEY=your-super-secret-key-here +WORKERS=4 + +# Database settings +DATABASE_URL=postgresql://user:password@db:5432/todoapp +REDIS_URL=redis://:password@redis:6379/0 +REDIS_PASSWORD=your-redis-password + +# Logging settings +LOG_LEVEL=info +LOG_FILE=/app/logs/app.log + +# Security settings +ALLOWED_HOSTS=["your-domain.com"] +CORS_ORIGINS=["https://your-frontend.com"] + +# Monitoring +SENTRY_DSN=https://your-sentry-dsn@sentry.io/project-id +``` + +### 生产部署命令 + +
+ +```console +# Deploy in production environment +$ docker-compose -f docker-compose.prod.yml --env-file .env.prod up -d + +# Scaling (app instance scaling) +$ docker-compose -f docker-compose.prod.yml up -d --scale app=3 + +# Rolling update +$ docker-compose -f docker-compose.prod.yml build app +$ docker-compose -f docker-compose.prod.yml up -d --no-deps app + +# Safe shutdown before backup +$ docker-compose -f docker-compose.prod.yml down --timeout 30 +``` + +
+ +## 第 8 步:监控与日志 + +### Docker 容器资源监控 + +
+ +```console +# Check real-time resource usage +$ docker stats + +CONTAINER ID NAME CPU % MEM USAGE / LIMIT MEM % NET I/O BLOCK I/O PIDS +abc123def456 dockerized-todo-api 2.34% 128.5MiB / 1GiB 12.55% 1.23MB / 456kB 12.3MB / 4.56MB 15 +def456ghi789 dockerized-todo-nginx 0.12% 12.5MiB / 256MiB 4.88% 456kB / 1.23MB 1.23MB / 456kB 3 +ghi789jkl012 dockerized-todo-redis 1.45% 32.1MiB / 512MiB 6.27% 789kB / 2.34MB 4.56MB / 1.23MB 4 + +# Check specific container details +$ docker inspect dockerized-todo-api + +# Check container internal processes +$ docker-compose exec app ps aux +``` + +
+ +### 日志聚合与分析 + +```yaml +# docker-compose.logging.yml +version: '3.8' + +services: + # ELK Stack for log aggregation + elasticsearch: + image: docker.elastic.co/elasticsearch/elasticsearch:8.6.0 + environment: + - discovery.type=single-node + - xpack.security.enabled=false + volumes: + - elasticsearch_data:/usr/share/elasticsearch/data + networks: + - logging + + logstash: + image: docker.elastic.co/logstash/logstash:8.6.0 + volumes: + - ./logstash/pipeline:/usr/share/logstash/pipeline:ro + - ./logstash/config:/usr/share/logstash/config:ro + networks: + - logging + depends_on: + - elasticsearch + + kibana: + image: docker.elastic.co/kibana/kibana:8.6.0 + ports: + - "5601:5601" + environment: + - ELASTICSEARCH_HOSTS=http://elasticsearch:9200 + networks: + - logging + depends_on: + - elasticsearch + + # Fluentd for log collection + fluentd: + image: fluent/fluentd:v1.16-debian-1 + volumes: + - ./fluentd/conf:/fluentd/etc:ro + - /var/log:/var/log:ro + networks: + - logging + depends_on: + - elasticsearch + +volumes: + elasticsearch_data: + +networks: + logging: + driver: bridge +``` + +### Prometheus 指标收集 + +```python +# src/monitoring.py +from prometheus_client import Counter, Histogram, Gauge, generate_latest +from fastapi import Request, Response +import time + +# Define metrics +REQUEST_COUNT = Counter( + 'http_requests_total', + 'Total HTTP requests', + ['method', 'endpoint', 'status_code'] +) + +REQUEST_DURATION = Histogram( + 'http_request_duration_seconds', + 'HTTP request duration in seconds', + ['method', 'endpoint'] +) + +ACTIVE_CONNECTIONS = Gauge( + 'active_connections', + 'Number of active connections' +) + +async def metrics_middleware(request: Request, call_next): + """Prometheus metric collection middleware""" + start_time = time.time() + method = request.method + endpoint = request.url.path + + ACTIVE_CONNECTIONS.inc() + + try: + response = await call_next(request) + status_code = response.status_code + except Exception as e: + status_code = 500 + raise + finally: + duration = time.time() - start_time + REQUEST_DURATION.labels(method=method, endpoint=endpoint).observe(duration) + REQUEST_COUNT.labels(method=method, endpoint=endpoint, status_code=status_code).inc() + ACTIVE_CONNECTIONS.dec() + + return response + +@app.get("/metrics") +async def get_metrics(): + """Prometheus metric endpoint""" + return Response(generate_latest(), media_type="text/plain") +``` + +## 第 9 步:构建 CI/CD 流水线 + +### GitHub Actions 工作流(`.github/workflows/deploy.yml`) + +```yaml +name: Deploy to Production + +on: + push: + branches: [main] + pull_request: + branches: [main] + +env: + REGISTRY: ghcr.io + IMAGE_NAME: ${{ github.repository }} + +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: '3.12' + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -r requirements.txt + pip install pytest pytest-asyncio httpx + + - name: Run tests + run: | + pytest tests/ -v --cov=src --cov-report=xml + + - name: Upload coverage reports + uses: codecov/codecov-action@v3 + with: + file: ./coverage.xml + + build: + needs: test + runs-on: ubuntu-latest + if: github.event_name == 'push' && github.ref == 'refs/heads/main' + + steps: + - uses: actions/checkout@v4 + + - name: Log in to Container Registry + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract metadata + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + tags: | + type=ref,event=branch + type=ref,event=pr + type=sha + type=raw,value=latest + + - name: Build and push Docker image + uses: docker/build-push-action@v5 + with: + context: . + file: ./Dockerfile + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max + + deploy: + needs: build + runs-on: ubuntu-latest + if: github.event_name == 'push' && github.ref == 'refs/heads/main' + + steps: + - uses: actions/checkout@v4 + + - name: Deploy to production + uses: appleboy/ssh-action@v1.0.0 + with: + host: ${{ secrets.PROD_HOST }} + username: ${{ secrets.PROD_USERNAME }} + key: ${{ secrets.PROD_SSH_KEY }} + script: | + cd /opt/dockerized-todo-api + + # Pull new image + docker-compose -f docker-compose.prod.yml pull + + # Rolling update + docker-compose -f docker-compose.prod.yml up -d --no-deps app + + # Health check + sleep 30 + curl -f http://localhost/health || exit 1 + + # Clean up previous image + docker image prune -f +``` + +## 第 10 步:加强安全 + +### 容器安全设置 + +```dockerfile +# Add security enhancement to Dockerfile + +# Run as non-root user +USER appuser + +# Read-only root filesystem +# docker run --read-only --tmpfs /tmp dockerized-todo-api + +# Limit permissions +# docker run --cap-drop=ALL dockerized-todo-api + +# Network isolation +# docker run --network=none dockerized-todo-api +``` + +### Docker Compose 安全设置 + +```yaml +# Add security settings to docker-compose.yml +services: + app: + # ... existing settings ... + security_opt: + - no-new-privileges:true + cap_drop: + - ALL + cap_add: + - NET_BIND_SERVICE + read_only: true + tmpfs: + - /tmp + - /app/logs + user: "1000:1000" +``` + +### 机密管理 + +```yaml +# Add secrets settings to docker-compose.yml +version: '3.8' + +services: + app: + secrets: + - db_password + - api_key + environment: + - DB_PASSWORD_FILE=/run/secrets/db_password + - API_KEY_FILE=/run/secrets/api_key + +secrets: + db_password: + file: ./secrets/db_password.txt + api_key: + external: true +``` + +## 下一步 + +恭喜您完成了 Docker 容器化!接下来可以尝试: + +1. **[自定义响应处理](custom-response-handling.md)** —— 实现进阶的 API 响应格式 + + + + +## 小结 + +在本教程中,我们用 Docker 完成了: + +- ✅ 通过多阶段构建生成优化的容器镜像 +- ✅ 用 Docker Compose 搭建开发 / 生产环境 +- ✅ 配置 Nginx 反向代理与负载均衡 +- ✅ 构建健康检查与监控体系 +- ✅ 通过 CI/CD 流水线实现自动化部署 +- ✅ 配置生产级别的安全设置 +- ✅ 实现日志与指标收集系统 + +现在您可以安全、高效地把 FastAPI 应用部署到生产环境! diff --git a/docs/zh/tutorial/domain-starter.md b/docs/zh/tutorial/domain-starter.md new file mode 100644 index 0000000..8610fb6 --- /dev/null +++ b/docs/zh/tutorial/domain-starter.md @@ -0,0 +1,392 @@ +# 使用 `fastapi-domain-starter` 构建面向领域的 FastAPI + +按推荐的现代布局构建一个中型 FastAPI 服务 —— **每个业务概念在 `src/app/domains/` 下对应一个文件夹**。本教程会端到端走完 `fastapi-domain-starter` 模板:如何生成、每个顶层包的职责、自带的 `items` 示例如何接入,以及如何添加下一个领域。 + +## 您将学到的内容 + +- 使用 `fastkit startdemo fastapi-domain-starter` 生成项目 +- 布局中 `core`、`db`、`domains`、`tests` 各自的角色 +- 一个领域如何拆分为 router → service → repository → schemas → models +- 添加新领域的契约(复制 items 文件夹,注册路由) +- 自带的 `/health` 端点与 `/api/v1/items` CRUD 如何接入应用 + +## 前置条件 + +- Python 3.12+ +- 已安装 FastAPI-fastkit(`pip install fastapi-fastkit`) +- 熟悉 FastAPI 的基础概念(路径操作、pydantic 模式、依赖) + +如果这是您的第一个 FastAPI 项目,请先从 [构建基础 API 服务器](basic-api-server.md) 开始 —— 那篇教程使用更简单的 `fastapi-default` 模板。 + +## 第 1 步:生成项目 + +```console +$ fastkit startdemo fastapi-domain-starter +Enter the project name: orders-api +Enter the author name: Developer Kim +Enter the author email: developer@example.com +Enter the project description: Domain-oriented orders service +Select package manager (pip, uv, pdm, poetry) [uv]: uv +Do you want to proceed with project creation? [y/N]: y +``` + +`fastkit` 会部署模板、填入占位符、创建虚拟环境并安装依赖。完成后即可进入项目: + +```console +$ cd orders-api +$ bash scripts/run-server.sh # or: uvicorn src.app.main:app --reload +``` + +随后可在 查看 API 文档。 + +## 第 2 步:生成的目录树 + +``` +orders-api/ +├── README.md +├── pyproject.toml # PEP 621 metadata + [tool.fastapi-fastkit] +├── requirements.txt # pinned deps (template ships both files; you maintain them as you add packages) +├── .env # SECRET_KEY, ENVIRONMENT +├── .gitignore +├── scripts/ +│ ├── format.sh # black + isort +│ ├── lint.sh # black --check + isort --check + mypy +│ ├── run-server.sh # uvicorn src.app.main:app --reload +│ └── test.sh # pytest +├── src/ +│ ├── __init__.py +│ └── app/ # the application package +│ ├── __init__.py +│ ├── main.py # FastAPI() + middleware + api_router include +│ ├── core/ # cross-cutting configuration +│ │ ├── __init__.py +│ │ └── config.py # pydantic-settings (PROJECT_NAME, CORS, ...) +│ ├── db/ # persistence abstractions +│ │ ├── __init__.py +│ │ └── memory.py # InMemoryStore[T] generic key-value store +│ ├── api/ # transport-level routing +│ │ ├── __init__.py +│ │ ├── health.py # GET /health +│ │ └── router.py # aggregates health + every domain router +│ └── domains/ # business concepts (one folder each) +│ ├── __init__.py +│ └── items/ # the example domain +│ ├── __init__.py +│ ├── models.py # @dataclass Item (entity) +│ ├── schemas.py # ItemCreate, ItemRead (pydantic) +│ ├── repository.py # ItemRepository over InMemoryStore +│ ├── service.py # ItemService + ItemNotFoundError +│ └── router.py # APIRouter(prefix="/items") +└── tests/ + ├── __init__.py + ├── conftest.py # TestClient fixture, store reset + ├── test_health.py + └── test_items.py +``` + +需要内化的两个概念: + +1. **`src/app/`** 是 **应用包** —— 运行时导入的所有内容都在这里。测试也从这里导入(`from src.app.main import app`)。外层 `src/` 是为了让项目能 `pip install`。 +2. **`src/app/domains//`** 是 **每个概念的切片** —— 每个业务概念(items、orders、users……)拥有各自的 router / service / repository / schemas / models,且仅有这些。 + +## 第 3 步:每个顶层包的职责 + +### `src/app/core/` —— 配置 + +存放跨切面的应用配置。自带的 `config.py` 暴露一个 pydantic-settings 的 `Settings` 类,从 `.env` / 环境变量读取: + +```python +class Settings(BaseSettings): + PROJECT_NAME: str = "" + ENVIRONMENT: Literal["development", "staging", "production"] = "development" + SECRET_KEY: str = secrets.token_urlsafe(32) + API_V1_PREFIX: str = "/api/v1" + BACKEND_CORS_ORIGINS: ... = [] + ... + +settings = Settings() +``` + +`main.py` 读取 `settings.PROJECT_NAME`、`settings.API_V1_PREFIX` 与 `settings.all_cors_origins` 来接入 FastAPI 应用。 + +**什么时候应往 `core/` 添加内容:** 任何与单个领域无关的内容 —— 全局设置、结构化日志、自定义中间件、安全工具等。 + +### `src/app/db/` —— 持久化边界 + +存放对数据存储的抽象。starter 自带 `memory.py` —— 一个进程内的 `InMemoryStore[T]`,对实体类型进行了泛化。每个领域的 repository 都封装一个 `InMemoryStore`,后续切换到 SQLAlchemy / 异步驱动也只是一次受控的变更:只需要重写 repository。 + +```python +class InMemoryStore(Generic[T]): + def list(self) -> Iterable[T]: ... + def get(self, id_: int) -> Optional[T]: ... + def add(self, item: T) -> int: ... + def replace(self, id_: int, item: T) -> bool: ... + def delete(self, id_: int) -> bool: ... + def clear(self) -> None: ... +``` + +**什么时候应扩展 `db/`:** 当您从 `InMemoryStore` 迁移走时,加上一个 `session.py`,放真实数据库的 session 工厂。保留同样的公共方法形状(`list` / `get` / `add` / ……),这样领域的 repository 不必修改其内部契约。 + +### `src/app/api/` —— 传输层路由 + +由两部分组成: + +- `health.py` —— 一个小的 `APIRouter`,暴露 `GET /health` 并返回 `{"status": "ok"}`。无副作用,适合做存活探针。 +- `router.py` —— **顶层聚合器**。它纳入健康检查路由器以及每个领域的路由器,再把这个合并后的 `api_router` 挂载在 FastAPI 应用的 `/api/v1` 下: + +```python +# src/app/api/router.py +api_router = APIRouter() +api_router.include_router(health.router) +api_router.include_router(items_router.router) +``` + +```python +# src/app/main.py +app.include_router(api_router, prefix=settings.API_V1_PREFIX) +``` + +**为何要在这里聚合:** 添加新领域时,只需要编辑 `src/app/api/router.py` 来注册它的路由器,`main.py` 从不需要改动。 + +### `src/app/domains//` —— 业务切片 + +随着项目成长,大多数代码会落在这里。每个领域拥有五个文件: + +| 文件 | 角色 | +|---|---| +| `models.py` | 领域实体(starter 中是 `@dataclass`;以后可以换成 SQLAlchemy / SQLModel)。表示内部形状 —— 并非线上格式。 | +| `schemas.py` | API 输入输出模式(pydantic)。与实体分离,这样线上格式可以独立演进而不影响领域逻辑。 | +| `repository.py` | 数据访问层。用领域实体类型的方法封装存储,是切换持久化方案的接缝。 | +| `service.py` | 业务逻辑。Router 调用 `service`,绝不直接调用 `repository`。领域专属异常(如 `ItemNotFoundError`)放在这里。 | +| `router.py` | HTTP 传输。负责在 pydantic 模式 ↔ service 调用之间翻译,把领域异常转换为 `HTTPException`。 | + +**依赖方向**是 `router → service → repository → store`。每层只依赖其下一层。Schema 由 router 与 service 引用;model 由 repository 与 service 引用。 + +### `tests/` + +镜像运行时布局 —— 对每个值得固定行为的对外面都对应一个测试模块。starter 自带: + +- `conftest.py` —— autouse fixture,在测试之间重置 items 存储;另外一个 `client` fixture 封装了 `TestClient(app)`。 +- `test_health.py` —— 验证 `GET /api/v1/health` 返回 200 + `{"status": "ok"}`。 +- `test_items.py` —— 对 items 端点做完整 CRUD 覆盖,包括对未知 id 返回 404、对无效负载返回 422。 + +运行测试: + +```console +$ bash scripts/test.sh # or: pytest +``` + +## 第 4 步:走读自带的 `items` 领域 + +这个示例领域是一个针对极小实体的 CRUD: + +```python +# src/app/domains/items/models.py +@dataclass +class Item: + id: int + name: str + price: float + in_stock: bool = True +``` + +API 模式把输入形状与输出形状分开,这样可以加入服务端控制的字段(`id`)与校验(price ≥ 0): + +```python +# src/app/domains/items/schemas.py +class ItemCreate(BaseModel): + name: str = Field(min_length=1, max_length=120) + price: float = Field(ge=0) + in_stock: bool = True + +class ItemRead(BaseModel): + id: int + name: str + price: float + in_stock: bool + model_config = ConfigDict(from_attributes=True) +``` + +Repository 封装内存存储,并在插入时分配 id: + +```python +# src/app/domains/items/repository.py +class ItemRepository: + def __init__(self, store: Optional[InMemoryStore[Item]] = None) -> None: + self._store = store if store is not None else _store + + def add(self, name: str, price: float, in_stock: bool = True) -> Item: + item = Item(id=0, name=name, price=price, in_stock=in_stock) + new_id = self._store.add(item) + item.id = new_id + return item + # list_all / get / replace / delete / reset elided +``` + +Service 层用于沉淀业务规则。现在它只是带一个自定义异常的薄包装,但未来的业务策略会落在这里(比如「不能删除已存在于未关闭订单中的商品」): + +```python +# src/app/domains/items/service.py +class ItemNotFoundError(Exception): ... + +class ItemService: + def __init__(self, repository: Optional[ItemRepository] = None) -> None: + self._repository = repository if repository is not None else ItemRepository() + + def get_item(self, item_id: int) -> Item: + item = self._repository.get(item_id) + if item is None: + raise ItemNotFoundError(f"Item {item_id} does not exist") + return item + # list_items / create_item / replace_item / delete_item elided +``` + +Router 是唯一了解 HTTP 的那一层。注意它通过 FastAPI 的 `Depends(...)` 接收 service,以便测试中覆盖;并把 `ItemNotFoundError` 映射成 `HTTPException(404)`: + +```python +# src/app/domains/items/router.py +router = APIRouter(prefix="/items", tags=["items"]) + +def get_item_service() -> ItemService: + return ItemService() + +@router.get("/{item_id}", response_model=ItemRead) +def get_item(item_id: int, service: ItemService = Depends(get_item_service)) -> ItemRead: + try: + return ItemRead.model_validate(service.get_item(item_id)) + except ItemNotFoundError as exc: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(exc)) +``` + +完整的 router 暴露: + +| 方法 | 路径 | 作用 | +|---|---|---| +| `GET` | `/api/v1/items` | 列出 item | +| `GET` | `/api/v1/items/{item_id}` | 读取单个 | +| `POST` | `/api/v1/items` | 创建(返回 201) | +| `PUT` | `/api/v1/items/{item_id}` | 替换 | +| `DELETE` | `/api/v1/items/{item_id}` | 删除(返回 204) | +| `GET` | `/api/v1/health` | 存活探针 | + +试一下: + +```console +$ curl -X POST http://127.0.0.1:8000/api/v1/items \ + -H 'Content-Type: application/json' \ + -d '{"name":"Mug","price":9.5,"in_stock":true}' +{"id":1,"name":"Mug","price":9.5,"in_stock":true} + +$ curl http://127.0.0.1:8000/api/v1/items +[{"id":1,"name":"Mug","price":9.5,"in_stock":true}] + +$ curl http://127.0.0.1:8000/api/v1/items/999 +{"detail":"Item 999 does not exist"} +``` + +## 第 5 步:添加下一个领域 + +starter 的设计目标是**让添加领域变成一次复制 + 改名操作**。假设您想在 `items` 之外加一个 `users` 领域: + +### 1. 复制 `items/` 文件夹 + +```console +$ cp -r src/app/domains/items src/app/domains/users +``` + +### 2. 重写实体、模式以及各文件中的类名 + +```python +# src/app/domains/users/models.py +from dataclasses import dataclass + +@dataclass +class User: + id: int + email: str + is_active: bool = True +``` + +```python +# src/app/domains/users/schemas.py +from pydantic import BaseModel, ConfigDict, Field + +class UserCreate(BaseModel): + # Plain ``str`` keeps the snippet drop-in safe. To use pydantic's + # built-in email validation instead, install the optional dependency + # (``pip install 'pydantic[email]'`` — pulls in ``email-validator``) + # and switch ``str`` to ``EmailStr``. + email: str = Field(min_length=3, max_length=320) + is_active: bool = True + +class UserRead(BaseModel): + id: int + email: str + is_active: bool + model_config = ConfigDict(from_attributes=True) +``` + +在 `models.py`、`schemas.py`、`repository.py`、`service.py` 与 `router.py` 中把 `Item → User`、`ItemNotFoundError → UserNotFoundError`、`ItemRepository → UserRepository`、`ItemService → UserService` 全部改名。别忘了 router 中的 `prefix="/items"` → `prefix="/users"`,以及 `tags=["items"]` → `tags=["users"]`。 + +Repository 仍可保留同样的 `InMemoryStore` 模式 —— 它本身就是基于实体类型的泛型: + +```python +# src/app/domains/users/repository.py +_store: InMemoryStore[User] = InMemoryStore() + +class UserRepository: + def __init__(self, store: Optional[InMemoryStore[User]] = None) -> None: + self._store = store if store is not None else _store + # ... same shape as ItemRepository ... +``` + +### 3. 更新领域的 `__init__.py` + +items 领域会重新导出其模块,以便调用方可以写 `from src.app.domains.items import service`。对 users 也照样做: + +```python +# src/app/domains/users/__init__.py +from src.app.domains.users import ( # noqa: F401 + models, + repository, + router, + schemas, + service, +) +``` + +### 4. 在聚合器中注册路由 + +这是 **`domains/users/` 之外您唯一需要修改的文件**: + +```python +# src/app/api/router.py +from src.app.api import health +from src.app.domains.items import router as items_router +from src.app.domains.users import router as users_router # ← add + +api_router = APIRouter() +api_router.include_router(health.router) +api_router.include_router(items_router.router) +api_router.include_router(users_router.router) # ← add +``` + +重启服务器后,您会在 `/docs` 中看到挂载好的 `/api/v1/users`。 + +### 5. 添加测试 + +把 `tests/test_items.py` 镜像成 `tests/test_users.py` —— 同样基于 client 的结构,只是请求新端点。`conftest.py` 中 autouse 的存储重置 fixture 已经能保证每个测试的隔离。 + +如果第二个领域也使用 `InMemoryStore`,可以扩展该 fixture 让它也重置该存储,或者每个领域各保留一个 fixture。 + +## 第 6 步:接下来去哪里 + +- [架构预设矩阵](../reference/preset-feature-matrix.md) 展示了 `fastkit init --interactive` 对每种预设会生成什么,包括 `domain-starter` 下哪些功能选择需要手动接入。 +- [`fastapi-default` 教程](basic-api-server.md) 介绍了分层备选方案,如果您想在选定之前比较一下布局,这里很有帮助。 +- 对于数据库集成,[数据库集成教程](database-integration.md) 展示了 PostgreSQL + SQLAlchemy + Alembic 的模式。同样的思路可以套用到 `src/app/db/` 与各领域的 `repository.py`。 + +## 回顾 + +- **生成**:`fastkit startdemo fastapi-domain-starter` → `bash scripts/run-server.sh` → 在 `/docs` 查看文档。 +- **布局**:`core/` 放配置,`db/` 放持久化抽象,`domains//` 放业务切片,`api/router.py` 作为唯一聚合点,`tests/` 镜像运行时模块。 +- **添加领域**:复制 `items/`,改实体 / 模式 / 类名,更新 `__init__.py` 的重新导出,在 `src/app/api/router.py` 注册路由,新增测试模块。`main.py` 完全不需要改动。 diff --git a/docs/zh/tutorial/first-project.md b/docs/zh/tutorial/first-project.md new file mode 100644 index 0000000..e075bb0 --- /dev/null +++ b/docs/zh/tutorial/first-project.md @@ -0,0 +1,1252 @@ +# 您的第一个项目 + +使用 FastAPI-fastkit 构建一个完整的博客 API,包含用户管理、文章创建与评论系统。 + +## 项目概览 + +在本教程中,我们将创建一个具备以下特性的 **博客 API**: + +- **用户管理**:注册、认证与用户资料 +- **文章管理**:创建、读取、更新与删除博客文章 +- **评论系统**:为博客文章添加评论 +- **数据校验**:稳健的输入校验与错误处理 +- **API 文档**:自动生成的 OpenAPI 文档 +- **测试**:完整的测试套件 + +### 您将学到的内容 + +本教程结束时,您将理解: + +- 进阶的 FastAPI-fastkit 项目结构 +- 与 SQLAlchemy 的数据库集成 +- 用户认证与授权 +- 复杂的数据关系 +- 错误处理与校验 +- 测试的最佳实践 + +## 前置条件 + +开始前请确认: + +- 已完成 [入门指南](getting-started.md) 教程 +- 对 REST API 有基础理解 +- 已安装 Python 3.12+ +- 已准备好文本编辑器或 IDE + +## 第 1 步:创建项目 + +让我们用 **STANDARD** 栈来创建一个新项目,以获得数据库支持: + +
+ +```console +$ fastkit init +Enter the project name: blog-api +Enter the author name: Your Name +Enter the author email: your.email@example.com +Enter the project description: A complete blog API with users, posts, and comments + + Project Information +┌──────────────┬─────────────────────────────────────────┐ +│ Project Name │ blog-api │ +│ Author │ Your Name │ +│ Author Email │ your.email@example.com │ +│ Description │ A complete blog API with users, posts, │ +│ │ and comments │ +└──────────────┴─────────────────────────────────────────┘ + +Available Stacks and Dependencies: + MINIMAL Stack +┌──────────────┬───────────────────┐ +│ Dependency 1 │ fastapi │ +│ Dependency 2 │ uvicorn │ +│ Dependency 3 │ pydantic │ +│ Dependency 4 │ pydantic-settings │ +└──────────────┴───────────────────┘ + + STANDARD Stack +┌──────────────┬───────────────────┐ +│ Dependency 1 │ fastapi │ +│ Dependency 2 │ uvicorn │ +│ Dependency 3 │ sqlalchemy │ +│ Dependency 4 │ alembic │ +│ Dependency 5 │ pytest │ +│ Dependency 6 │ pydantic │ +│ Dependency 7 │ pydantic-settings │ +└──────────────┴───────────────────┘ + +Select stack (minimal, standard, full): standard + +Available Package Managers: + Package Managers +┌────────┬────────────────────────────────────────────┐ +│ PIP │ Standard Python package manager │ +│ UV │ Fast Python package manager │ +│ PDM │ Modern Python dependency management │ +│ POETRY │ Python dependency management and packaging │ +└────────┴────────────────────────────────────────────┘ + +Select package manager (pip, uv, pdm, poetry) [uv]: uv +Do you want to proceed with project creation? [y/N]: y + +✨ FastAPI project 'blog-api' has been created successfully! +``` + +
+ +## 第 2 步:配置项目 + +进入项目目录并激活虚拟环境: + +
+ +```console +$ cd blog-api +$ source .venv/bin/activate +``` + +
+ +## 第 3 步:添加所需路由 + +为博客 API 添加主要资源: + +
+ +```console +$ fastkit addroute users blog-api +✨ Successfully added new route 'users' to project 'blog-api' + +$ fastkit addroute posts blog-api +✨ Successfully added new route 'posts' to project 'blog-api' + +$ fastkit addroute comments blog-api +✨ Successfully added new route 'comments' to project 'blog-api' +``` + +
+ +## 第 4 步:设计数据模型 + +让我们设计数据模式,先更新用户模式,使其更贴近真实场景。 + +### 更新用户模式 + +编辑 `src/schemas/users.py`: + +```python +from typing import Optional, List +from datetime import datetime +from pydantic import BaseModel, EmailStr, Field + +class UserBase(BaseModel): + email: EmailStr + username: str = Field(..., min_length=3, max_length=50) + full_name: Optional[str] = None + bio: Optional[str] = Field(None, max_length=500) + is_active: bool = True + +class UserCreate(UserBase): + password: str = Field(..., min_length=8) + +class UserUpdate(BaseModel): + email: Optional[EmailStr] = None + username: Optional[str] = Field(None, min_length=3, max_length=50) + full_name: Optional[str] = None + bio: Optional[str] = Field(None, max_length=500) + is_active: Optional[bool] = None + +class User(UserBase): + id: int + created_at: datetime + posts_count: int = 0 + + class Config: + from_attributes = True + +class UserInDB(User): + hashed_password: str +``` + +### 创建文章模式 + +编辑 `src/schemas/posts.py`: + +```python +from typing import Optional, List +from datetime import datetime +from pydantic import BaseModel, Field + +class PostBase(BaseModel): + title: str = Field(..., min_length=1, max_length=200) + content: str = Field(..., min_length=1) + published: bool = True + +class PostCreate(PostBase): + pass + +class PostUpdate(BaseModel): + title: Optional[str] = Field(None, min_length=1, max_length=200) + content: Optional[str] = Field(None, min_length=1) + published: Optional[bool] = None + +class Post(PostBase): + id: int + author_id: int + created_at: datetime + updated_at: datetime + comments_count: int = 0 + + class Config: + from_attributes = True + +class PostWithAuthor(Post): + author: "User" + +class PostWithComments(Post): + comments: List["Comment"] = [] + +# Import to avoid circular imports +from src.schemas.users import User +from src.schemas.comments import Comment +PostWithAuthor.model_rebuild() +PostWithComments.model_rebuild() +``` + +### 创建评论模式 + +编辑 `src/schemas/comments.py`: + +```python +from typing import Optional +from datetime import datetime +from pydantic import BaseModel, Field + +class CommentBase(BaseModel): + content: str = Field(..., min_length=1, max_length=1000) + +class CommentCreate(CommentBase): + post_id: int + +class CommentUpdate(BaseModel): + content: Optional[str] = Field(None, min_length=1, max_length=1000) + +class Comment(CommentBase): + id: int + post_id: int + author_id: int + created_at: datetime + updated_at: datetime + + class Config: + from_attributes = True + +class CommentWithAuthor(Comment): + author: "User" + +# Import to avoid circular imports +from src.schemas.users import User +CommentWithAuthor.model_rebuild() +``` + +## 第 5 步:实现进阶的 CRUD 操作 + +### 增强的用户 CRUD + +更新 `src/crud/users.py`: + +```python +from typing import List, Optional +from datetime import datetime +import hashlib +from src.schemas.users import UserCreate, UserUpdate, UserInDB + +class UsersCRUD: + def __init__(self): + self._users: List[UserInDB] = [] + self._next_id = 1 + + def _hash_password(self, password: str) -> str: + """Simple password hashing (use bcrypt in production)""" + return hashlib.sha256(password.encode()).hexdigest() + + def _verify_password(self, plain_password: str, hashed_password: str) -> bool: + """Verify password against hash""" + return self._hash_password(plain_password) == hashed_password + + def get_all(self) -> List[UserInDB]: + """Get all users""" + return [user for user in self._users if user.is_active] + + def get_by_id(self, user_id: int) -> Optional[UserInDB]: + """Get user by ID""" + return next((user for user in self._users if user.id == user_id), None) + + def get_by_email(self, email: str) -> Optional[UserInDB]: + """Get user by email""" + return next((user for user in self._users if user.email == email), None) + + def get_by_username(self, username: str) -> Optional[UserInDB]: + """Get user by username""" + return next((user for user in self._users if user.username == username), None) + + def create(self, user: UserCreate) -> UserInDB: + """Create a new user with validation""" + # Check for duplicates + if self.get_by_email(user.email): + raise ValueError("Email already registered") + if self.get_by_username(user.username): + raise ValueError("Username already taken") + + new_user = UserInDB( + id=self._next_id, + email=user.email, + username=user.username, + full_name=user.full_name, + bio=user.bio, + is_active=user.is_active, + created_at=datetime.now(), + posts_count=0, + hashed_password=self._hash_password(user.password) + ) + self._next_id += 1 + self._users.append(new_user) + return new_user + + def update(self, user_id: int, user_update: UserUpdate) -> Optional[UserInDB]: + """Update an existing user""" + user = self.get_by_id(user_id) + if not user: + return None + + # Check for duplicates on email/username changes + update_data = user_update.dict(exclude_unset=True) + if "email" in update_data and update_data["email"] != user.email: + if self.get_by_email(update_data["email"]): + raise ValueError("Email already registered") + + if "username" in update_data and update_data["username"] != user.username: + if self.get_by_username(update_data["username"]): + raise ValueError("Username already taken") + + for field, value in update_data.items(): + setattr(user, field, value) + + return user + + def delete(self, user_id: int) -> bool: + """Soft delete user (deactivate)""" + user = self.get_by_id(user_id) + if user: + user.is_active = False + return True + return False + + def authenticate(self, email: str, password: str) -> Optional[UserInDB]: + """Authenticate user by email and password""" + user = self.get_by_email(email) + if user and self._verify_password(password, user.hashed_password): + return user + return None + +users_crud = UsersCRUD() +``` + +### 文章 CRUD + +更新 `src/crud/posts.py`: + +```python +from typing import List, Optional +from datetime import datetime +from src.schemas.posts import PostCreate, PostUpdate, Post + +class PostsCRUD: + def __init__(self): + self._posts: List[Post] = [] + self._next_id = 1 + + def get_all(self, skip: int = 0, limit: int = 100, published_only: bool = True) -> List[Post]: + """Get all posts with pagination""" + posts = self._posts + if published_only: + posts = [post for post in posts if post.published] + return posts[skip:skip + limit] + + def get_by_id(self, post_id: int) -> Optional[Post]: + """Get post by ID""" + return next((post for post in self._posts if post.id == post_id), None) + + def get_by_author(self, author_id: int, skip: int = 0, limit: int = 100) -> List[Post]: + """Get posts by author""" + author_posts = [post for post in self._posts if post.author_id == author_id] + return author_posts[skip:skip + limit] + + def create(self, post: PostCreate, author_id: int) -> Post: + """Create a new post""" + now = datetime.now() + new_post = Post( + id=self._next_id, + title=post.title, + content=post.content, + published=post.published, + author_id=author_id, + created_at=now, + updated_at=now, + comments_count=0 + ) + self._next_id += 1 + self._posts.append(new_post) + + # Update author's post count + from src.crud.users import users_crud + author = users_crud.get_by_id(author_id) + if author: + author.posts_count += 1 + + return new_post + + def update(self, post_id: int, post_update: PostUpdate, author_id: int) -> Optional[Post]: + """Update an existing post""" + post = self.get_by_id(post_id) + if not post or post.author_id != author_id: + return None + + update_data = post_update.dict(exclude_unset=True) + for field, value in update_data.items(): + setattr(post, field, value) + + post.updated_at = datetime.now() + return post + + def delete(self, post_id: int, author_id: int) -> bool: + """Delete a post""" + post = self.get_by_id(post_id) + if post and post.author_id == author_id: + self._posts.remove(post) + + # Update author's post count + from src.crud.users import users_crud + author = users_crud.get_by_id(author_id) + if author: + author.posts_count = max(0, author.posts_count - 1) + + return True + return False + + def search(self, query: str, skip: int = 0, limit: int = 100) -> List[Post]: + """Search posts by title or content""" + query_lower = query.lower() + matching_posts = [ + post for post in self._posts + if post.published and ( + query_lower in post.title.lower() or + query_lower in post.content.lower() + ) + ] + return matching_posts[skip:skip + limit] + +posts_crud = PostsCRUD() +``` + +### 评论 CRUD + +更新 `src/crud/comments.py`: + +```python +from typing import List, Optional +from datetime import datetime +from src.schemas.comments import CommentCreate, CommentUpdate, Comment + +class CommentsCRUD: + def __init__(self): + self._comments: List[Comment] = [] + self._next_id = 1 + + def get_all(self) -> List[Comment]: + """Get all comments""" + return self._comments + + def get_by_id(self, comment_id: int) -> Optional[Comment]: + """Get comment by ID""" + return next((comment for comment in self._comments if comment.id == comment_id), None) + + def get_by_post(self, post_id: int, skip: int = 0, limit: int = 100) -> List[Comment]: + """Get comments for a specific post""" + post_comments = [comment for comment in self._comments if comment.post_id == post_id] + return post_comments[skip:skip + limit] + + def get_by_author(self, author_id: int, skip: int = 0, limit: int = 100) -> List[Comment]: + """Get comments by author""" + author_comments = [comment for comment in self._comments if comment.author_id == author_id] + return author_comments[skip:skip + limit] + + def create(self, comment: CommentCreate, author_id: int) -> Comment: + """Create a new comment""" + # Verify post exists + from src.crud.posts import posts_crud + post = posts_crud.get_by_id(comment.post_id) + if not post: + raise ValueError("Post not found") + + now = datetime.now() + new_comment = Comment( + id=self._next_id, + content=comment.content, + post_id=comment.post_id, + author_id=author_id, + created_at=now, + updated_at=now + ) + self._next_id += 1 + self._comments.append(new_comment) + + # Update post's comment count + post.comments_count += 1 + + return new_comment + + def update(self, comment_id: int, comment_update: CommentUpdate, author_id: int) -> Optional[Comment]: + """Update an existing comment""" + comment = self.get_by_id(comment_id) + if not comment or comment.author_id != author_id: + return None + + update_data = comment_update.dict(exclude_unset=True) + for field, value in update_data.items(): + setattr(comment, field, value) + + comment.updated_at = datetime.now() + return comment + + def delete(self, comment_id: int, author_id: int) -> bool: + """Delete a comment""" + comment = self.get_by_id(comment_id) + if comment and comment.author_id == author_id: + self._comments.remove(comment) + + # Update post's comment count + from src.crud.posts import posts_crud + post = posts_crud.get_by_id(comment.post_id) + if post: + post.comments_count = max(0, post.comments_count - 1) + + return True + return False + +comments_crud = CommentsCRUD() +``` + +## 第 6 步:实现进阶的 API 路由 + +### 增强的用户路由 + +更新 `src/api/routes/users.py`: + +```python +from typing import List +from fastapi import APIRouter, HTTPException, status, Depends, Query +from src.schemas.users import User, UserCreate, UserUpdate +from src.crud.users import users_crud + +router = APIRouter() + +# 获取当前用户的辅助函数(教程里做了简化) +def get_current_user_id() -> int: + # 在真实项目中,这里应当校验 JWT 并返回用户 ID + return 1 # 教程中仅用于演示 + +@router.get("/", response_model=List[User]) +def read_users( + skip: int = Query(0, ge=0), + limit: int = Query(100, ge=1, le=100) +): + """分页获取所有用户""" + users = users_crud.get_all()[skip:skip + limit] + return [User(**user.dict()) for user in users] + +@router.post("/", response_model=User, status_code=status.HTTP_201_CREATED) +def create_user(user: UserCreate): + """Register a new user""" + try: + new_user = users_crud.create(user) + return User(**new_user.dict()) + except ValueError as e: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=str(e) + ) + +@router.get("/{user_id}", response_model=User) +def read_user(user_id: int): + """Get a specific user""" + user = users_crud.get_by_id(user_id) + if not user: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=f"User with id {user_id} not found" + ) + return User(**user.dict()) + +@router.put("/{user_id}", response_model=User) +def update_user( + user_id: int, + user_update: UserUpdate, + current_user_id: int = Depends(get_current_user_id) +): + """Update user profile""" + if user_id != current_user_id: + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail="You can only update your own profile" + ) + + try: + updated_user = users_crud.update(user_id, user_update) + if not updated_user: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="User not found" + ) + return User(**updated_user.dict()) + except ValueError as e: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=str(e) + ) + +@router.delete("/{user_id}", status_code=status.HTTP_204_NO_CONTENT) +def delete_user( + user_id: int, + current_user_id: int = Depends(get_current_user_id) +): + """Deactivate user account""" + if user_id != current_user_id: + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail="You can only delete your own account" + ) + + success = users_crud.delete(user_id) + if not success: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="User not found" + ) + +@router.post("/login") +def login(email: str, password: str): + """Authenticate user""" + user = users_crud.authenticate(email, password) + if not user: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Invalid email or password" + ) + + # In a real app, return JWT token + return { + "message": "Login successful", + "user_id": user.id, + "username": user.username + } +``` + +### 增强的文章路由 + +更新 `src/api/routes/posts.py`: + +```python +from typing import List, Optional +from fastapi import APIRouter, HTTPException, status, Depends, Query +from src.schemas.posts import Post, PostCreate, PostUpdate +from src.crud.posts import posts_crud + +router = APIRouter() + +def get_current_user_id() -> int: + return 1 # 教程中做了简化 + +@router.get("/", response_model=List[Post]) +def read_posts( + skip: int = Query(0, ge=0), + limit: int = Query(100, ge=1, le=100), + search: Optional[str] = Query(None) +): + """获取文章列表,并支持可选搜索""" + if search: + posts = posts_crud.search(search, skip, limit) + else: + posts = posts_crud.get_all(skip, limit) + return posts + +@router.post("/", response_model=Post, status_code=status.HTTP_201_CREATED) +def create_post( + post: PostCreate, + current_user_id: int = Depends(get_current_user_id) +): + """Create a new blog post""" + new_post = posts_crud.create(post, current_user_id) + return new_post + +@router.get("/{post_id}", response_model=Post) +def read_post(post_id: int): + """Get a specific post""" + post = posts_crud.get_by_id(post_id) + if not post: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Post not found" + ) + return post + +@router.put("/{post_id}", response_model=Post) +def update_post( + post_id: int, + post_update: PostUpdate, + current_user_id: int = Depends(get_current_user_id) +): + """Update a blog post""" + updated_post = posts_crud.update(post_id, post_update, current_user_id) + if not updated_post: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Post not found or you don't have permission to edit it" + ) + return updated_post + +@router.delete("/{post_id}", status_code=status.HTTP_204_NO_CONTENT) +def delete_post( + post_id: int, + current_user_id: int = Depends(get_current_user_id) +): + """Delete a blog post""" + success = posts_crud.delete(post_id, current_user_id) + if not success: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Post not found or you don't have permission to delete it" + ) + +@router.get("/author/{author_id}", response_model=List[Post]) +def read_posts_by_author( + author_id: int, + skip: int = Query(0, ge=0), + limit: int = Query(100, ge=1, le=100) +): + """Get posts by a specific author""" + posts = posts_crud.get_by_author(author_id, skip, limit) + return posts +``` + +### 增强的评论路由 + +更新 `src/api/routes/comments.py`: + +```python +from typing import List +from fastapi import APIRouter, HTTPException, status, Depends, Query +from src.schemas.comments import Comment, CommentCreate, CommentUpdate +from src.crud.comments import comments_crud + +router = APIRouter() + +def get_current_user_id() -> int: + return 1 # 教程中做了简化 + +@router.get("/", response_model=List[Comment]) +def read_comments( + skip: int = Query(0, ge=0), + limit: int = Query(100, ge=1, le=100) +): + """Get all comments""" + comments = comments_crud.get_all()[skip:skip + limit] + return comments + +@router.post("/", response_model=Comment, status_code=status.HTTP_201_CREATED) +def create_comment( + comment: CommentCreate, + current_user_id: int = Depends(get_current_user_id) +): + """Create a new comment""" + try: + new_comment = comments_crud.create(comment, current_user_id) + return new_comment + except ValueError as e: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=str(e) + ) + +@router.get("/{comment_id}", response_model=Comment) +def read_comment(comment_id: int): + """Get a specific comment""" + comment = comments_crud.get_by_id(comment_id) + if not comment: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Comment not found" + ) + return comment + +@router.put("/{comment_id}", response_model=Comment) +def update_comment( + comment_id: int, + comment_update: CommentUpdate, + current_user_id: int = Depends(get_current_user_id) +): + """Update a comment""" + updated_comment = comments_crud.update(comment_id, comment_update, current_user_id) + if not updated_comment: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Comment not found or you don't have permission to edit it" + ) + return updated_comment + +@router.delete("/{comment_id}", status_code=status.HTTP_204_NO_CONTENT) +def delete_comment( + comment_id: int, + current_user_id: int = Depends(get_current_user_id) +): + """Delete a comment""" + success = comments_crud.delete(comment_id, current_user_id) + if not success: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Comment not found or you don't have permission to delete it" + ) + +@router.get("/post/{post_id}", response_model=List[Comment]) +def read_comments_by_post( + post_id: int, + skip: int = Query(0, ge=0), + limit: int = Query(100, ge=1, le=100) +): + """Get comments for a specific post""" + comments = comments_crud.get_by_post(post_id, skip, limit) + return comments + +@router.get("/author/{author_id}", response_model=List[Comment]) +def read_comments_by_author( + author_id: int, + skip: int = Query(0, ge=0), + limit: int = Query(100, ge=1, le=100) +): + """Get comments by a specific author""" + comments = comments_crud.get_by_author(author_id, skip, limit) + return comments +``` + +## 第 7 步:测试您的博客 API + +启动服务器,测试完整的博客 API: + +
+ +```console +$ fastkit runserver +INFO: Uvicorn running on http://127.0.0.1:8000 +``` + +
+ +### 测试用户注册 + +
+ +```console +$ curl -X POST "http://127.0.0.1:8000/api/v1/users/" \ + -H "Content-Type: application/json" \ + -d '{ + "email": "john@example.com", + "username": "john_doe", + "full_name": "John Doe", + "bio": "Software developer and blogger", + "password": "securepassword123" + }' + +{ + "id": 1, + "email": "john@example.com", + "username": "john_doe", + "full_name": "John Doe", + "bio": "Software developer and blogger", + "is_active": true, + "created_at": "2023-12-07T10:30:00", + "posts_count": 0 +} +``` + +
+ +### 测试用户登录 + +
+ +```console +$ curl -X POST "http://127.0.0.1:8000/api/v1/users/login" \ + -H "Content-Type: application/json" \ + -d '{ + "email": "john@example.com", + "password": "securepassword123" + }' + +{ + "message": "Login successful", + "user_id": 1, + "username": "john_doe" +} +``` + +
+ +### 测试文章创建 + +
+ +```console +$ curl -X POST "http://127.0.0.1:8000/api/v1/posts/" \ + -H "Content-Type: application/json" \ + -d '{ + "title": "My First Blog Post", + "content": "This is the content of my first blog post. It is about learning FastAPI with FastAPI-fastkit!", + "published": true + }' + +{ + "id": 1, + "title": "My First Blog Post", + "content": "This is the content of my first blog post. It is about learning FastAPI with FastAPI-fastkit!", + "published": true, + "author_id": 1, + "created_at": "2023-12-07T10:35:00", + "updated_at": "2023-12-07T10:35:00", + "comments_count": 0 +} +``` + +
+ +### 测试评论创建 + +
+ +```console +$ curl -X POST "http://127.0.0.1:8000/api/v1/comments/" \ + -H "Content-Type: application/json" \ + -d '{ + "content": "Great post! I learned a lot from this.", + "post_id": 1 + }' + +{ + "id": 1, + "content": "Great post! I learned a lot from this.", + "post_id": 1, + "author_id": 1, + "created_at": "2023-12-07T10:40:00", + "updated_at": "2023-12-07T10:40:00" +} +``` + +
+ +### 测试搜索功能 + +
+ +```console +$ curl "http://127.0.0.1:8000/api/v1/posts/?search=FastAPI" + +[ + { + "id": 1, + "title": "My First Blog Post", + "content": "This is the content of my first blog post. It is about learning FastAPI with FastAPI-fastkit!", + "published": true, + "author_id": 1, + "created_at": "2023-12-07T10:35:00", + "updated_at": "2023-12-07T10:35:00", + "comments_count": 1 + } +] +``` + +
+ +## 第 8 步:API 文档 + +访问 [http://127.0.0.1:8000/docs](http://127.0.0.1:8000/docs) 查看完整的 API 文档。您现在应该能看到: + +- **Users**:注册、登录、资料管理 +- **Posts**:CRUD 操作、搜索、按作者筛选 +- **Comments**:CRUD 操作、按文章 / 作者筛选 +- **Items**:原始示例端点 + +文档会展示: + +- 所有可用端点 +- 请求 / 响应模式 +- 数据校验规则 +- 错误响应 + +## 第 9 步:编写测试 + +为博客 API 编写完善的测试。创建 `tests/test_blog_api.py`: + +```python +from fastapi.testclient import TestClient +from src.main import app + +client = TestClient(app) + +class TestUserAPI: + def test_create_user(self): + user_data = { + "email": "test@example.com", + "username": "testuser", + "full_name": "Test User", + "bio": "Test bio", + "password": "testpassword123" + } + response = client.post("/api/v1/users/", json=user_data) + assert response.status_code == 201 + data = response.json() + assert data["email"] == user_data["email"] + assert data["username"] == user_data["username"] + assert "id" in data + assert "hashed_password" not in data # Should not expose password + + def test_duplicate_email(self): + # First user + user_data1 = { + "email": "duplicate@example.com", + "username": "user1", + "password": "password123" + } + response1 = client.post("/api/v1/users/", json=user_data1) + assert response1.status_code == 201 + + # Second user with same email + user_data2 = { + "email": "duplicate@example.com", + "username": "user2", + "password": "password123" + } + response2 = client.post("/api/v1/users/", json=user_data2) + assert response2.status_code == 400 + assert "Email already registered" in response2.json()["detail"] + + def test_login(self): + # Create user first + user_data = { + "email": "login@example.com", + "username": "loginuser", + "password": "loginpassword123" + } + client.post("/api/v1/users/", json=user_data) + + # Test login + login_data = { + "email": "login@example.com", + "password": "loginpassword123" + } + response = client.post("/api/v1/users/login", json=login_data) + assert response.status_code == 200 + data = response.json() + assert "user_id" in data + assert data["username"] == "loginuser" + +class TestPostAPI: + def test_create_post(self): + post_data = { + "title": "Test Post", + "content": "This is a test post content", + "published": True + } + response = client.post("/api/v1/posts/", json=post_data) + assert response.status_code == 201 + data = response.json() + assert data["title"] == post_data["title"] + assert data["content"] == post_data["content"] + assert "id" in data + assert "author_id" in data + + def test_read_posts(self): + response = client.get("/api/v1/posts/") + assert response.status_code == 200 + data = response.json() + assert isinstance(data, list) + + def test_search_posts(self): + # Create a post with specific content + post_data = { + "title": "FastAPI Tutorial", + "content": "Learn how to build APIs with FastAPI", + "published": True + } + client.post("/api/v1/posts/", json=post_data) + + # Search for the post + response = client.get("/api/v1/posts/?search=FastAPI") + assert response.status_code == 200 + data = response.json() + assert len(data) > 0 + assert any("FastAPI" in post["title"] or "FastAPI" in post["content"] for post in data) + +class TestCommentAPI: + def test_create_comment(self): + # Create a post first + post_data = { + "title": "Post for Comments", + "content": "This post will receive comments", + "published": True + } + post_response = client.post("/api/v1/posts/", json=post_data) + post_id = post_response.json()["id"] + + # Create comment + comment_data = { + "content": "This is a test comment", + "post_id": post_id + } + response = client.post("/api/v1/comments/", json=comment_data) + assert response.status_code == 201 + data = response.json() + assert data["content"] == comment_data["content"] + assert data["post_id"] == post_id + + def test_get_comments_by_post(self): + # Create post and comment first + post_data = { + "title": "Post with Comments", + "content": "This post has comments", + "published": True + } + post_response = client.post("/api/v1/posts/", json=post_data) + post_id = post_response.json()["id"] + + comment_data = { + "content": "Comment on post", + "post_id": post_id + } + client.post("/api/v1/comments/", json=comment_data) + + # Get comments for the post + response = client.get(f"/api/v1/comments/post/{post_id}") + assert response.status_code == 200 + data = response.json() + assert len(data) > 0 + assert all(comment["post_id"] == post_id for comment in data) + +# Run the tests +if __name__ == "__main__": + import pytest + pytest.main([__file__]) +``` + +### 运行测试 + +
+ +```console +$ python -m pytest tests/test_blog_api.py -v +======================== test session starts ======================== +tests/test_blog_api.py::TestUserAPI::test_create_user PASSED +tests/test_blog_api.py::TestUserAPI::test_duplicate_email PASSED +tests/test_blog_api.py::TestUserAPI::test_login PASSED +tests/test_blog_api.py::TestPostAPI::test_create_post PASSED +tests/test_blog_api.py::TestPostAPI::test_read_posts PASSED +tests/test_blog_api.py::TestPostAPI::test_search_posts PASSED +tests/test_blog_api.py::TestCommentAPI::test_create_comment PASSED +tests/test_blog_api.py::TestCommentAPI::test_get_comments_by_post PASSED +======================== 8 passed in 1.23s ======================== +``` + +
+ +## 您已构建的成果 + +恭喜!您已经成功构建了一个完整的博客 API,包含: + +### ✅ 已实现的功能 + +- **用户管理** + - 带校验的用户注册 + - 用户认证(登录) + - 资料管理 + - 重复防护 + +- **博客文章** + - 创建、读取、更新、删除文章 + - 按作者筛选 + - 搜索功能 + - 发布 / 草稿状态 + +- **评论系统** + - 为文章添加评论 + - 按文章或作者查看评论 + - 评论管理 + +- **数据校验** + - 邮箱校验 + - 密码长度要求 + - 内容长度上限 + - 必填字段校验 + +- **错误处理** + - 合适的 HTTP 状态码 + - 描述清晰的错误信息 + - 输入校验错误 + +- **API 文档** + - 自动生成 OpenAPI + - 交互式测试界面 + - 请求 / 响应模式 + +- **测试** + - 完整的测试覆盖 + - 针对所有端点的单元测试 + - 边界情况测试 + +## 下一步 + +### 可能的增强方向 + +1. **真正的认证** + - 实现 JWT token + - 使用 bcrypt 做密码哈希 + - 基于角色的权限 + +2. **数据库集成** + - 使用 PostgreSQL 或 MySQL + - 实现完整的数据库模型 + - 添加数据库迁移 + +3. **进阶特性** + - 图片上传 + - 邮件通知 + - 文章分类 / 标签 + - 点赞 / 反对系统 + +4. **生产可用** + - 加入日志 + - 实现缓存 + - 加入限流 + - 环境配置 + +### 继续学习 + +1. **[使用模板](../user-guide/using-templates.md)**:探索 `fastapi-psql-orm` 模板,获得数据库集成 +2. **[添加路由](../user-guide/adding-routes.md)**:学习更进阶的路由模式 +3. **[参与贡献](../contributing/development-setup.md)**:为 FastAPI-fastkit 贡献代码 + +!!! tip "您已掌握的最佳实践" + - **模块化架构**:通过 schemas、CRUD、routes 做关注点分离 + - **数据校验**:使用 Pydantic 实现稳健的输入校验 + - **错误处理**:合适的 HTTP 状态码与错误信息 + - **测试**:对所有功能做完善的测试覆盖 + - **文档**:善用自动生成的 API 文档能力 + +您现在已经具备使用 FastAPI-fastkit 构建生产级 API 的能力!🚀 diff --git a/docs/zh/tutorial/getting-started.md b/docs/zh/tutorial/getting-started.md new file mode 100644 index 0000000..fac4085 --- /dev/null +++ b/docs/zh/tutorial/getting-started.md @@ -0,0 +1,564 @@ +# 入门指南 + +这是一份完整的 FastAPI-fastkit 上手教程,会带您一步步从安装走到运行第一个 API,大约 15 分钟即可完成。 + +## 前置条件 + +开始之前,请确认您已具备: + +- 已在系统上安装 **Python 3.12 及以上** +- 具备 **Python 基础知识**(变量、函数、类) +- 可以访问 **终端 / 命令行** +- 一个 **文本编辑器或 IDE**(VS Code、PyCharm 等) + +## 第 1 步:安装 + +首先安装 FastAPI-fastkit。建议使用虚拟环境以保持项目隔离。 + +### 方案 A:使用 pip(传统) + +
+ +```console +$ pip install fastapi-fastkit +---> 100% +Successfully installed fastapi-fastkit +``` + +
+ +### 方案 B:使用 UV(推荐 —— 更快) + +UV 是一款高速的 Python 包管理器。若您尚未安装 UV: + +
+ +```console +# Install UV first +$ curl -LsSf https://astral.sh/uv/install.sh | sh + +# Then install FastAPI-fastkit +$ uv pip install fastapi-fastkit +---> 100% +Successfully installed fastapi-fastkit +``` + +
+ +### 方案 C:使用虚拟环境 + +
+ +```console +$ python -m venv fastapi-env +$ source fastapi-env/bin/activate # On Windows: fastapi-env\Scripts\activate +$ pip install fastapi-fastkit +``` + +
+ +### 验证安装 + +确认 FastAPI-fastkit 已正确安装: + +
+ +```console +$ fastkit --version +FastAPI-fastkit version 1.0.0 +``` + +
+ +## 第 2 步:创建您的第一个项目 + +接下来用交互式的 `init` 命令创建第一个 FastAPI 项目: + +
+ +```console +$ fastkit init +Enter the project name: my-first-api +Enter the author name: Your Name +Enter the author email: your.email@example.com +Enter the project description: My first FastAPI project + + Project Information +┌──────────────┬─────────────────────────┐ +│ Project Name │ my-first-api │ +│ Author │ Your Name │ +│ Author Email │ your.email@example.com │ +│ Description │ My first FastAPI project│ +└──────────────┴─────────────────────────┘ + +Available Stacks and Dependencies: + MINIMAL Stack +┌──────────────┬───────────────────┐ +│ Dependency 1 │ fastapi │ +│ Dependency 2 │ uvicorn │ +│ Dependency 3 │ pydantic │ +│ Dependency 4 │ pydantic-settings │ +└──────────────┴───────────────────┘ + + STANDARD Stack +┌──────────────┬───────────────────┐ +│ Dependency 1 │ fastapi │ +│ Dependency 2 │ uvicorn │ +│ Dependency 3 │ sqlalchemy │ +│ Dependency 4 │ alembic │ +│ Dependency 5 │ pytest │ +│ Dependency 6 │ pydantic │ +│ Dependency 7 │ pydantic-settings │ +└──────────────┴───────────────────┘ + +Select stack (minimal, standard, full): minimal + +Available Package Managers: + Package Managers +┌────────┬────────────────────────────────────────────┐ +│ PIP │ Standard Python package manager │ +│ UV │ Fast Python package manager │ +│ PDM │ Modern Python dependency management │ +│ POETRY │ Python dependency management and packaging │ +└────────┴────────────────────────────────────────────┘ + +Select package manager (pip, uv, pdm, poetry) [uv]: uv +Do you want to proceed with project creation? [y/N]: y + +Creating virtual environment... +Installing dependencies... +✨ FastAPI project 'my-first-api' has been created successfully! +``` + +
+ +!!! note "技术栈选择" + 本教程为简化起见选择了 **MINIMAL**。在真实项目中,可以考虑 **STANDARD**(包含数据库支持)或 **FULL**(包含后台任务)。 + +## 第 3 步:进入项目目录 + +进入刚创建好的项目目录: + +
+ +```console +$ cd my-first-api +$ ls -la +total 32 +drwxr-xr-x 8 user user 256 Dec 7 10:30 . +drwxr-xr-x 3 user user 96 Dec 7 10:30 .. +drwxr-xr-x 5 user user 160 Dec 7 10:30 .venv +-rw-r--r-- 1 user user 156 Dec 7 10:30 README.md +-rw-r--r-- 1 user user 243 Dec 7 10:30 requirements.txt +drwxr-xr-x 3 user user 96 Dec 7 10:30 scripts +-rw-r--r-- 1 user user 1245 Dec 7 10:30 setup.py +drwxr-xr-x 8 user user 256 Dec 7 10:30 src +drwxr-xr-x 3 user user 96 Dec 7 10:30 tests +``` + +
+ +## 第 4 步:激活虚拟环境 + +您的项目已预先配置好虚拟环境,激活它: + +
+ +```console +$ source .venv/bin/activate # On Windows: .venv\Scripts\activate +(my-first-api) $ +``` + +
+ +注意您的终端提示符现在会显示 `(my-first-api)`,表示虚拟环境已激活。 + +## 第 5 步:启动开发服务器 + +接下来是激动人心的部分 —— 启动 FastAPI 服务器: + +
+ +```console +$ fastkit runserver +INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) +INFO: Started reloader process [28720] using StatReload +INFO: Started server process [28722] +INFO: Waiting for application startup. +INFO: Application startup complete. +``` + +
+ +🎉 **恭喜!** 您的 FastAPI 服务器已经运行起来了。 + +## 第 6 步:测试 API + +下面用几种方式测试您的 API: + +### 方式 1:浏览器 + +打开浏览器,访问: + +- **API 主端点**:[http://127.0.0.1:8000](http://127.0.0.1:8000) + +您会看到: +```json +{"message": "Hello World"} +``` + +### 方式 2:交互式 API 文档 + +访问 FastAPI 自动生成的 API 文档: + +- **Swagger UI**:[http://127.0.0.1:8000/docs](http://127.0.0.1:8000/docs) +- **ReDoc**:[http://127.0.0.1:8000/redoc](http://127.0.0.1:8000/redoc) + +Swagger UI 尤为实用,您可以: + +- 查看所有可用端点 +- 直接在浏览器中测试端点 +- 查看请求 / 响应的模式 +- 下载 OpenAPI 规范 + +### 方式 3:命令行 + +另开一个终端(保持服务器运行),用 curl 测试: + +
+ +```console +$ curl http://127.0.0.1:8000 +{"message":"Hello World"} + +$ curl http://127.0.0.1:8000/api/v1/items/ +[] + +$ curl -X POST "http://127.0.0.1:8000/api/v1/items/" \ + -H "Content-Type: application/json" \ + -d '{"title": "My First Item", "description": "This is a test item"}' +{ + "id": 1, + "title": "My First Item", + "description": "This is a test item" +} +``` + +
+ +## 第 7 步:了解您的项目结构 + +来看看 FastAPI-fastkit 为您生成了哪些内容: + +
+ +```console +$ tree src +src/ +├── __init__.py +├── main.py # FastAPI 应用入口 +├── core/ +│ ├── __init__.py +│ └── config.py # 应用配置 +├── api/ +│ ├── __init__.py +│ ├── api.py # 主 API 路由 +│ └── routes/ +│ ├── __init__.py +│ └── items.py # items API 端点 +├── crud/ +│ ├── __init__.py +│ └── items.py # items 相关业务逻辑 +├── schemas/ +│ ├── __init__.py +│ └── items.py # 数据校验模式 +└── mocks/ + ├── __init__.py + └── mock_items.json # 示例数据 +``` + +
+ +### 关键文件说明 + +**`src/main.py`** —— 应用的核心: +```python +from fastapi import FastAPI +from src.api.api import api_router +from src.core.config import settings + +app = FastAPI( + title=settings.PROJECT_NAME, + version=settings.VERSION, + openapi_url=f"{settings.API_V1_STR}/openapi.json" +) + +app.include_router(api_router, prefix=settings.API_V1_STR) + +@app.get("/") +def read_root(): + return {"message": "Hello World"} +``` + +**`src/core/config.py`** —— 应用设置: +```python +from pydantic_settings import BaseSettings + +class Settings(BaseSettings): + PROJECT_NAME: str = "my-first-api" + VERSION: str = "1.0.0" + API_V1_STR: str = "/api/v1" + + class Config: + env_file = ".env" + +settings = Settings() +``` + +**`src/api/routes/items.py`** —— API 端点: +```python +from typing import List +from fastapi import APIRouter, HTTPException +from src.schemas.items import Item, ItemCreate, ItemUpdate +from src.crud.items import items_crud + +router = APIRouter() + +@router.get("/", response_model=List[Item]) +def read_items(): + """Get all items""" + return items_crud.get_all() + +@router.post("/", response_model=Item) +def create_item(item: ItemCreate): + """Create a new item""" + return items_crud.create(item) +``` + +## 第 8 步:添加您的第一个自定义路由 + +接下来添加一个新的 API 路由,练习刚学到的内容: + +
+ +```console +$ fastkit addroute users my-first-api + Adding New Route +┌──────────────────┬──────────────────────────────────────────┐ +│ Project │ my-first-api │ +│ Route Name │ users │ +│ Target Directory │ ~/my-first-api │ +└──────────────────┴──────────────────────────────────────────┘ + +Do you want to add route 'users' to project 'my-first-api'? [Y/n]: y + +✨ Successfully added new route 'users' to project 'my-first-api' +``` + +
+ +服务器会自动重启,您将拥有以下新端点: + +- `GET /api/v1/users/` —— 获取所有用户 +- `POST /api/v1/users/` —— 创建新用户 +- `GET /api/v1/users/{user_id}` —— 获取指定用户 +- 等等…… + +### 测试新路由 + +
+ +```console +$ curl -X POST "http://127.0.0.1:8000/api/v1/users/" \ + -H "Content-Type: application/json" \ + -d '{"title": "John Doe", "description": "Software Developer"}' +{ + "id": 1, + "title": "John Doe", + "description": "Software Developer" +} + +$ curl http://127.0.0.1:8000/api/v1/users/ +[ + { + "id": 1, + "title": "John Doe", + "description": "Software Developer" + } +] +``` + +
+ +## 第 9 步:探索并修改代码 + +我们做一个小修改,看看代码是如何运作的。 + +### 修改欢迎信息 + +在编辑器中打开 `src/main.py`,修改根端点: + +```python +@app.get("/") +def read_root(): + return {"message": "Welcome to my first FastAPI application!"} +``` + +保存文件。由于启用了自动重载,服务器会自动重启。 + +### 测试改动 + +
+ +```console +$ curl http://127.0.0.1:8000 +{"message":"Welcome to my first FastAPI application!"} +``` + +
+ +### 添加一个新端点 + +在 `src/main.py` 中加入一个简单的端点: + +```python +@app.get("/hello/{name}") +def say_hello(name: str): + return {"message": f"Hello, {name}!"} +``` + +### 测试新端点 + +
+ +```console +$ curl http://127.0.0.1:8000/hello/World +{"message":"Hello, World!"} + +$ curl http://127.0.0.1:8000/hello/FastAPI +{"message":"Hello, FastAPI!"} +``` + +
+ +## 第 10 步:运行测试 + +您的项目已预置好测试,运行它们: + +
+ +```console +$ python -m pytest +======================== test session starts ======================== +collected 5 items + +tests/test_items.py::test_create_item PASSED +tests/test_items.py::test_read_items PASSED +tests/test_items.py::test_read_item PASSED +tests/test_items.py::test_update_item PASSED +tests/test_items.py::test_delete_item PASSED + +======================== 5 passed in 0.45s ======================== +``` + +
+ +## 理解核心概念 + +### 1. FastAPI 应用结构 + +FastAPI-fastkit 采用 **模块化架构**: + +- **`main.py`**:应用入口与全局端点 +- **`api/`**:API 路由组织 +- **`core/`**:应用配置与设置 +- **`crud/`**:业务逻辑与数据操作 +- **`schemas/`**:数据校验与序列化 +- **`tests/`**:自动化测试 + +### 2. 依赖管理 + +您的项目采用现代 Python 依赖管理: + +- **虚拟环境**:隔离的 Python 环境 +- **requirements.txt**:列出所有依赖 +- **自动安装**:在创建项目时自动安装依赖 + +### 3. 开发服务器 + +FastAPI-fastkit 使用 **Uvicorn** 作为 ASGI 服务器: + +- **自动重载**:代码变更时自动重启 +- **快速启动**:开发迭代迅速 +- **可用于生产**:开发与生产使用同一款服务器 + +### 4. API 文档 + +FastAPI 会自动生成: + +- **OpenAPI 规范**:业界标准的 API 文档 +- **Swagger UI**:交互式测试界面 +- **ReDoc**:另一种文档视图 + +## 下一步 + +恭喜!您已成功完成: + +✅ 安装 FastAPI-fastkit +✅ 创建第一个项目 +✅ 启动开发服务器 +✅ 测试 API 端点 +✅ 添加新路由 +✅ 修改现有代码 +✅ 运行测试 + +### 继续学习 + +1. **[您的第一个项目](first-project.md)**:构建一个带有进阶特性的完整博客 API +2. **[添加路由](../user-guide/adding-routes.md)**:学习创建复杂的 API 端点 +3. **[使用模板](../user-guide/using-templates.md)**:探索预构建的项目模板 + +### 多多动手 + +尝试以下挑战: + +1. **添加校验**:修改 schema,加入数据校验规则 +2. **自定义响应**:更改路由的响应格式 +3. **环境变量**:使用 `.env` 文件做配置 +4. **添加中间件**:实现 CORS 或认证 +5. **数据库集成**:升级到 STANDARD 栈以获得数据库支持 + +### 常见问题与解决方案 + +**服务器无法启动:** + +- 检查是否处于项目目录中 +- 确认虚拟环境已激活 +- 确认代码没有语法错误 + +**导入错误:** + +- 确认所有 `__init__.py` 文件存在 +- 检查导入路径是否正确 +- 确认正在使用虚拟环境 + +**端口已被占用:** +```console +$ fastkit runserver --port 8080 +``` + +## 您已掌握的最佳实践 + +1. **虚拟环境**:始终使用隔离的环境 +2. **项目结构**:遵循组织良好的模块化架构 +3. **自动重载**:利用开发服务器实现快速迭代 +4. **API 文档**:善用自动文档生成能力 +5. **测试**:在开发过程中定期运行测试 + +!!! tip "开发小贴士" + - 编码时让开发服务器一直运行 + - 使用交互式文档(`/docs`)测试您的 API + - 关注终端中的报错信息 + - 经常把代码提交到版本控制 + +您已经准备好用 FastAPI-fastkit 构建出色的 API 了!🚀 diff --git a/docs/zh/tutorial/mcp-integration.md b/docs/zh/tutorial/mcp-integration.md new file mode 100644 index 0000000..e26df37 --- /dev/null +++ b/docs/zh/tutorial/mcp-integration.md @@ -0,0 +1,1730 @@ +# MCP(Model Context Protocol)集成 + +学习如何把 Model Context Protocol(MCP)与 FastAPI 集成,构建一个让 AI 模型能把 API 端点作为工具使用的系统。本节我们将通过 `fastapi-mcp` 模板实现一套完整的 AI 集成 API,涵盖认证、权限管理与 MCP 服务器实现。 + +## 您将学到的内容 + +- Model Context Protocol(MCP)的概念与实现 +- 构建基于 JWT 的认证体系 +- 实现基于角色的访问控制(RBAC) +- 暴露并管理 MCP 工具 +- 与 AI 模型间的安全 API 通信 +- 用户会话与上下文管理 + +## 前置条件 + +- 完成 [自定义响应处理教程](custom-response-handling.md) +- 理解 JWT 与 OAuth2 的基本概念 +- 熟悉与 AI / LLM 模型的 API 通信 +- 对 MCP 协议有基础认识 + +## 什么是 Model Context Protocol(MCP)? + +MCP 是一种标准化协议,让 AI 模型可以与外部系统进行交互。 + +### 传统方式 vs MCP 方式 + +**传统方式(直接 API 调用):** +``` +AI Model → HTTP Request → API Server → Response +``` + +**MCP 方式:** +``` +AI Model → MCP Client → MCP Server (FastAPI) → Safe Tool Execution → Response +``` + +### MCP 的优势 + +- **安全性**:统一的认证与权限管理 +- **标准化**:提供一致的接口 +- **上下文管理**:基于会话的状态维护 +- **工具抽象**:把复杂 API 暴露为简单工具 + +## 第 1 步:创建 MCP 集成项目 + +使用 `fastapi-mcp` 模板创建项目: + +
+ +```console +$ fastkit startdemo fastapi-mcp +Enter the project name: ai-integrated-api +Enter the author name: Developer Kim +Enter the author email: developer@example.com +Enter the project description: MCP-based API server integrated with AI models +Deploying FastAPI project using 'fastapi-mcp' template + + Project Information +┌──────────────┬─────────────────────────────────────────────┐ +│ Project Name │ ai-integrated-api │ +│ Author │ Developer Kim │ +│ Author Email │ developer@example.com │ +│ Description │ MCP-based API server integrated with AI models │ +└──────────────┴─────────────────────────────────────────────┘ + + Template Dependencies +┌──────────────┬────────────────┐ +│ Dependency 1 │ fastapi │ +│ Dependency 2 │ uvicorn │ +│ Dependency 3 │ pydantic │ +│ Dependency 4 │ python-jose │ +│ Dependency 5 │ passlib │ +│ Dependency 6 │ python-multipart│ +│ Dependency 7 │ mcp │ +└──────────────┴────────────────┘ + +Select package manager (pip, uv, pdm, poetry) [uv]: uv +Do you want to proceed with project creation? [y/N]: y + +✨ FastAPI project 'ai-integrated-api' from 'fastapi-mcp' has been created successfully! +``` + +
+ +## 第 2 步:项目结构分析 + +让我们看看生成项目的结构: + +``` +ai-integrated-api/ +├── src/ +│ ├── main.py # FastAPI application +│ ├── auth/ +│ │ ├── __init__.py +│ │ ├── models.py # Authentication-related data models +│ │ ├── jwt_handler.py # JWT token processing +│ │ ├── dependencies.py # Authentication dependencies +│ │ └── routes.py # Authentication router +│ ├── mcp/ +│ │ ├── __init__.py +│ │ ├── server.py # MCP server implementation +│ │ ├── tools.py # MCP tool definitions +│ │ └── client.py # MCP client (for testing) +│ ├── api/ +│ │ ├── __init__.py +│ │ ├── api.py # API router collection +│ │ └── routes/ +│ │ ├── items.py # Item management API +│ │ ├── users.py # User management API +│ │ └── admin.py # Admin API +│ ├── schemas/ +│ │ ├── __init__.py +│ │ ├── auth.py # Authentication schemas +│ │ ├── users.py # User schemas +│ │ └── items.py # Item schemas +│ └── core/ +│ ├── __init__.py +│ ├── config.py # Configuration +│ ├── database.py # Database (in-memory) +│ └── security.py # Security configuration +└── tests/ + ├── test_auth.py # Authentication tests + ├── test_mcp.py # MCP tests + └── test_integration.py # Integration tests +``` + +## 第 3 步:实现认证系统 + +### JWT 令牌处理(`src/auth/jwt_handler.py`) + +```python +from datetime import datetime, timedelta +from typing import Optional, Dict, Any +from jose import JWTError, jwt +from passlib.context import CryptContext + +from src.core.config import settings + +# 密码哈希处理 +pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") + +def verify_password(plain_password: str, hashed_password: str) -> bool: + """校验密码""" + return pwd_context.verify(plain_password, hashed_password) + +def get_password_hash(password: str) -> str: + """生成密码哈希""" + return pwd_context.hash(password) + +def create_access_token(data: Dict[str, Any], expires_delta: Optional[timedelta] = None) -> str: + """生成访问令牌""" + to_encode = data.copy() + + if expires_delta: + expire = datetime.utcnow() + expires_delta + else: + expire = datetime.utcnow() + timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES) + + to_encode.update({"exp": expire, "iat": datetime.utcnow()}) + + encoded_jwt = jwt.encode( + to_encode, + settings.SECRET_KEY, + algorithm=settings.ALGORITHM + ) + + return encoded_jwt + +def create_refresh_token(user_id: str) -> str: + """生成刷新令牌""" + data = {"sub": user_id, "type": "refresh"} + expire = datetime.utcnow() + timedelta(days=settings.REFRESH_TOKEN_EXPIRE_DAYS) + + to_encode = data.copy() + to_encode.update({"exp": expire, "iat": datetime.utcnow()}) + + return jwt.encode( + to_encode, + settings.SECRET_KEY, + algorithm=settings.ALGORITHM + ) + +def decode_token(token: str) -> Optional[Dict[str, Any]]: + """解析令牌""" + try: + payload = jwt.decode( + token, + settings.SECRET_KEY, + algorithms=[settings.ALGORITHM] + ) + return payload + except JWTError: + return None + +def verify_token(token: str, token_type: str = "access") -> Optional[str]: + """校验令牌并返回用户 ID""" + payload = decode_token(token) + + if not payload: + return None + + # 校验令牌类型 + if token_type == "refresh" and payload.get("type") != "refresh": + return None + + user_id = payload.get("sub") + if not user_id: + return None + + return user_id + +class TokenManager: + """令牌管理类""" + + def __init__(self): + self.blacklisted_tokens = set() + + def blacklist_token(self, token: str): + """将令牌加入黑名单""" + self.blacklisted_tokens.add(token) + + def is_blacklisted(self, token: str) -> bool: + """检查令牌是否在黑名单中""" + return token in self.blacklisted_tokens + + def create_token_pair(self, user_id: str, user_role: str) -> Dict[str, str]: + """创建访问令牌与刷新令牌对""" + access_token_data = { + "sub": user_id, + "role": user_role, + "type": "access" + } + + access_token = create_access_token(access_token_data) + refresh_token = create_refresh_token(user_id) + + return { + "access_token": access_token, + "refresh_token": refresh_token, + "token_type": "bearer" + } + +# Global token manager +token_manager = TokenManager() +``` + +### 用户模型与数据库(`src/auth/models.py`) + +```python +from typing import List, Optional, Dict, Any +from pydantic import BaseModel, EmailStr +from enum import Enum +from datetime import datetime + +class UserRole(str, Enum): + """User roles""" + ADMIN = "admin" + USER = "user" + AI_AGENT = "ai_agent" + READONLY = "readonly" + +class Permission(str, Enum): + """Permissions""" + READ_ITEMS = "read:items" + WRITE_ITEMS = "write:items" + DELETE_ITEMS = "delete:items" + MANAGE_USERS = "manage:users" + USE_MCP_TOOLS = "use:mcp_tools" + ADMIN_MCP = "admin:mcp" + +class User(BaseModel): + """User model""" + id: str + email: EmailStr + username: str + full_name: Optional[str] = None + role: UserRole + permissions: List[Permission] + is_active: bool = True + created_at: datetime + last_login: Optional[datetime] = None + api_key: Optional[str] = None # For MCP client + +class UserInDB(User): + """User model for database storage""" + hashed_password: str + +class UserCreate(BaseModel): + """User creation schema""" + email: EmailStr + username: str + password: str + full_name: Optional[str] = None + role: UserRole = UserRole.USER + +class UserUpdate(BaseModel): + """User update schema""" + email: Optional[EmailStr] = None + username: Optional[str] = None + full_name: Optional[str] = None + role: Optional[UserRole] = None + is_active: Optional[bool] = None + +class LoginRequest(BaseModel): + """Login request schema""" + username: str + password: str + +class TokenResponse(BaseModel): + """Token response schema""" + access_token: str + refresh_token: str + token_type: str = "bearer" + expires_in: int + user: User + +# 按角色划分的默认权限映射 +ROLE_PERMISSIONS = { + UserRole.ADMIN: [ + Permission.READ_ITEMS, + Permission.WRITE_ITEMS, + Permission.DELETE_ITEMS, + Permission.MANAGE_USERS, + Permission.USE_MCP_TOOLS, + Permission.ADMIN_MCP + ], + UserRole.USER: [ + Permission.READ_ITEMS, + Permission.WRITE_ITEMS, + Permission.USE_MCP_TOOLS + ], + UserRole.AI_AGENT: [ + Permission.READ_ITEMS, + Permission.WRITE_ITEMS, + Permission.USE_MCP_TOOLS + ], + UserRole.READONLY: [ + Permission.READ_ITEMS + ] +} + +class UserDatabase: + """Memory-based user database""" + + def __init__(self): + self.users: Dict[str, UserInDB] = {} + self._init_default_users() + + def _init_default_users(self): + """Create default users""" + from src.auth.jwt_handler import get_password_hash + import uuid + + # Admin account + admin_id = str(uuid.uuid4()) + self.users[admin_id] = UserInDB( + id=admin_id, + email="admin@example.com", + username="admin", + full_name="System Administrator", + role=UserRole.ADMIN, + permissions=ROLE_PERMISSIONS[UserRole.ADMIN], + hashed_password=get_password_hash("admin123"), + created_at=datetime.utcnow(), + api_key=str(uuid.uuid4()) + ) + + # AI agent account + ai_id = str(uuid.uuid4()) + self.users[ai_id] = UserInDB( + id=ai_id, + email="ai@example.com", + username="ai_agent", + full_name="AI Assistant", + role=UserRole.AI_AGENT, + permissions=ROLE_PERMISSIONS[UserRole.AI_AGENT], + hashed_password=get_password_hash("ai123"), + created_at=datetime.utcnow(), + api_key=str(uuid.uuid4()) + ) + + def get_user_by_username(self, username: str) -> Optional[UserInDB]: + """Get user by username""" + return next( + (user for user in self.users.values() if user.username == username), + None + ) + + def get_user_by_id(self, user_id: str) -> Optional[UserInDB]: + """Get user by ID""" + return self.users.get(user_id) + + def get_user_by_api_key(self, api_key: str) -> Optional[UserInDB]: + """Get user by API key""" + return next( + (user for user in self.users.values() if user.api_key == api_key), + None + ) + + def create_user(self, user_create: UserCreate) -> UserInDB: + """Create user""" + import uuid + from src.auth.jwt_handler import get_password_hash + + user_id = str(uuid.uuid4()) + user = UserInDB( + id=user_id, + email=user_create.email, + username=user_create.username, + full_name=user_create.full_name, + role=user_create.role, + permissions=ROLE_PERMISSIONS[user_create.role], + hashed_password=get_password_hash(user_create.password), + created_at=datetime.utcnow(), + api_key=str(uuid.uuid4()) + ) + + self.users[user_id] = user + return user + + def update_user(self, user_id: str, user_update: UserUpdate) -> Optional[UserInDB]: + """Update user""" + if user_id not in self.users: + return None + + user = self.users[user_id] + update_data = user_update.dict(exclude_unset=True) + + for field, value in update_data.items(): + setattr(user, field, value) + + # Update permissions if role changed + if "role" in update_data: + user.permissions = ROLE_PERMISSIONS[user.role] + + return user + + def update_last_login(self, user_id: str): + """Update last login time""" + if user_id in self.users: + self.users[user_id].last_login = datetime.utcnow() + +# Global database instance +user_db = UserDatabase() +``` + +## 第 4 步:实现认证依赖 + +### 认证依赖(`src/auth/dependencies.py`) + +```python +from typing import Optional, List +from fastapi import Depends, HTTPException, status, Security +from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials, APIKeyHeader +from jose import JWTError + +from src.auth.jwt_handler import decode_token, token_manager +from src.auth.models import User, UserInDB, Permission, user_db + +# Security schema +security = HTTPBearer() +api_key_header = APIKeyHeader(name="X-API-Key", auto_error=False) + +async def get_current_user( + credentials: HTTPAuthorizationCredentials = Security(security) +) -> User: + """Get current authenticated user""" + credentials_exception = HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Could not validate credentials", + headers={"WWW-Authenticate": "Bearer"}, + ) + + try: + token = credentials.credentials + + # Check blacklist + if token_manager.is_blacklisted(token): + raise credentials_exception + + payload = decode_token(token) + if payload is None: + raise credentials_exception + + user_id: str = payload.get("sub") + if user_id is None: + raise credentials_exception + + except JWTError: + raise credentials_exception + + user = user_db.get_user_by_id(user_id) + if user is None: + raise credentials_exception + + if not user.is_active: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="Inactive user" + ) + + return User(**user.dict()) + +async def get_current_user_by_api_key( + api_key: Optional[str] = Security(api_key_header) +) -> Optional[User]: + """Authenticate user by API key""" + if not api_key: + return None + + user = user_db.get_user_by_api_key(api_key) + if not user or not user.is_active: + return None + + return User(**user.dict()) + +async def get_current_user_flexible( + token_user: Optional[User] = Depends(get_current_user), + api_key_user: Optional[User] = Depends(get_current_user_by_api_key) +) -> User: + """Authenticate user by token or API key (flexible authentication)""" + user = token_user or api_key_user + + if not user: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Authentication required" + ) + + return user + +def require_permissions(*required_permissions: Permission): + """Dependency requiring specific permissions""" + def permission_checker(current_user: User = Depends(get_current_user_flexible)) -> User: + for permission in required_permissions: + if permission not in current_user.permissions: + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail=f"Permission '{permission}' required" + ) + return current_user + + return permission_checker + +def require_roles(*required_roles): + """Dependency requiring specific roles""" + def role_checker(current_user: User = Depends(get_current_user_flexible)) -> User: + if current_user.role not in required_roles: + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail=f"Role must be one of: {', '.join(required_roles)}" + ) + return current_user + + return role_checker + +# Common permission dependencies +RequireAdmin = require_roles("admin") +RequireReadItems = require_permissions(Permission.READ_ITEMS) +RequireWriteItems = require_permissions(Permission.WRITE_ITEMS) +RequireDeleteItems = require_permissions(Permission.DELETE_ITEMS) +RequireMCPTools = require_permissions(Permission.USE_MCP_TOOLS) +RequireAdminMCP = require_permissions(Permission.ADMIN_MCP) +``` + +### 认证路由(`src/auth/routes.py`) + +```python +from datetime import timedelta +from fastapi import APIRouter, Depends, HTTPException, status +from fastapi.security import OAuth2PasswordRequestForm + +from src.auth.models import ( + User, UserCreate, UserUpdate, LoginRequest, TokenResponse, + user_db, UserRole +) +from src.auth.jwt_handler import ( + verify_password, token_manager, verify_token, create_access_token +) +from src.auth.dependencies import get_current_user, RequireAdmin +from src.core.config import settings + +router = APIRouter(prefix="/auth", tags=["authentication"]) + +@router.post("/register", response_model=User) +async def register_user(user_create: UserCreate): + """注册用户""" + # 检查用户名是否重复 + if user_db.get_user_by_username(user_create.username): + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="Username already registered" + ) + + # 第一个用户自动设为管理员 + if not user_db.users: + user_create.role = UserRole.ADMIN + + user = user_db.create_user(user_create) + return User(**user.dict()) + +@router.post("/login", response_model=TokenResponse) +async def login_user(form_data: OAuth2PasswordRequestForm = Depends()): + """用户登录""" + user = user_db.get_user_by_username(form_data.username) + + if not user or not verify_password(form_data.password, user.hashed_password): + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Incorrect username or password", + headers={"WWW-Authenticate": "Bearer"}, + ) + + if not user.is_active: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="Inactive user" + ) + + # 生成令牌 + tokens = token_manager.create_token_pair(user.id, user.role) + + # 更新最后登录时间 + user_db.update_last_login(user.id) + + return TokenResponse( + access_token=tokens["access_token"], + refresh_token=tokens["refresh_token"], + token_type=tokens["token_type"], + expires_in=settings.ACCESS_TOKEN_EXPIRE_MINUTES * 60, + user=User(**user.dict()) + ) + +@router.post("/refresh", response_model=dict) +async def refresh_token(refresh_token: str): + """刷新令牌""" + user_id = verify_token(refresh_token, "refresh") + + if not user_id: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Invalid refresh token" + ) + + user = user_db.get_user_by_id(user_id) + if not user or not user.is_active: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="User not found or inactive" + ) + + # 生成新的令牌对 + tokens = token_manager.create_token_pair(user.id, user.role) + + return { + "access_token": tokens["access_token"], + "refresh_token": tokens["refresh_token"], + "token_type": tokens["token_type"], + "expires_in": settings.ACCESS_TOKEN_EXPIRE_MINUTES * 60 + } + +@router.post("/logout") +async def logout_user(current_user: User = Depends(get_current_user)): + """用户登出""" + # 在实际实现中,这里应将令牌加入黑名单 + return {"message": "Successfully logged out"} + +@router.get("/me", response_model=User) +async def get_current_user_info(current_user: User = Depends(get_current_user)): + """获取当前用户信息""" + return current_user + +@router.put("/me", response_model=User) +async def update_current_user( + user_update: UserUpdate, + current_user: User = Depends(get_current_user) +): + """更新当前用户信息""" + # 普通用户不能修改角色 + if user_update.role and current_user.role != UserRole.ADMIN: + user_update.role = None + + updated_user = user_db.update_user(current_user.id, user_update) + if not updated_user: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="User not found" + ) + + return User(**updated_user.dict()) + +@router.get("/users", response_model=list[User]) +async def list_users(admin_user: User = Depends(RequireAdmin)): + """获取用户列表(仅管理员)""" + return [User(**user.dict()) for user in user_db.users.values()] + +@router.post("/users/{user_id}/generate-api-key") +async def generate_api_key( + user_id: str, + admin_user: User = Depends(RequireAdmin) +): + """创建用户 API Key(仅管理员)""" + import uuid + + user = user_db.get_user_by_id(user_id) + if not user: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="User not found" + ) + + # 生成新的 API Key + new_api_key = str(uuid.uuid4()) + user.api_key = new_api_key + + return { + "api_key": new_api_key, + "message": "API key generated successfully" + } +``` + +## 第 5 步:实现 MCP 服务器 + +### MCP 工具定义(`src/mcp/tools.py`) + +```python +from typing import Dict, Any, List, Optional +from pydantic import BaseModel, Field +from enum import Enum + +class ToolCategory(str, Enum): + """工具分类""" + DATA_MANAGEMENT = "data_management" + SEARCH = "search" + ANALYSIS = "analysis" + ADMIN = "admin" + +class MCPTool(BaseModel): + """MCP 工具定义""" + name: str = Field(..., description="工具名称") + description: str = Field(..., description="工具说明") + category: ToolCategory = Field(..., description="工具分类") + parameters: Dict[str, Any] = Field(default_factory=dict, description="参数结构") + required_permissions: List[str] = Field(default_factory=list, description="所需权限") + examples: List[Dict[str, Any]] = Field(default_factory=list, description="使用示例") + +class ToolRegistry: + """工具注册表""" + + def __init__(self): + self.tools: Dict[str, MCPTool] = {} + self._register_default_tools() + + def _register_default_tools(self): + """注册默认工具""" + + # 注册创建条目工具 + self.register_tool(MCPTool( + name="create_item", + description="Create a new item", + category=ToolCategory.DATA_MANAGEMENT, + parameters={ + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "Item name" + }, + "description": { + "type": "string", + "description": "Item description" + }, + "price": { + "type": "number", + "description": "Item price", + "minimum": 0 + }, + "category": { + "type": "string", + "description": "Item category" + } + }, + "required": ["name", "price"] + }, + required_permissions=["write:items"], + examples=[ + { + "name": "Notebook", + "description": "High-performance gaming notebook", + "price": 1500000, + "category": "electronics" + } + ] + )) + + # Search item tool + self.register_tool(MCPTool( + name="search_items", + description="Search for items", + category=ToolCategory.SEARCH, + parameters={ + "type": "object", + "properties": { + "query": { + "type": "string", + "description": "Search query" + }, + "category": { + "type": "string", + "description": "Category filter" + }, + "min_price": { + "type": "number", + "description": "Minimum price" + }, + "max_price": { + "type": "number", + "description": "Maximum price" + }, + "limit": { + "type": "integer", + "description": "Result count limit", + "default": 10, + "maximum": 100 + } + }, + "required": ["query"] + }, + required_permissions=["read:items"], + examples=[ + { + "query": "Notebook", + "category": "electronics", + "max_price": 2000000, + "limit": 5 + } + ] + )) + + # Analyze item tool + self.register_tool(MCPTool( + name="analyze_items", + description="Analyze item data", + category=ToolCategory.ANALYSIS, + parameters={ + "type": "object", + "properties": { + "analysis_type": { + "type": "string", + "enum": ["price_distribution", "category_breakdown", "trend_analysis"], + "description": "Analysis type" + }, + "date_range": { + "type": "object", + "properties": { + "start_date": {"type": "string", "format": "date"}, + "end_date": {"type": "string", "format": "date"} + }, + "description": "Analysis period" + } + }, + "required": ["analysis_type"] + }, + required_permissions=["read:items"], + examples=[ + { + "analysis_type": "price_distribution", + "date_range": { + "start_date": "2024-01-01", + "end_date": "2024-12-31" + } + } + ] + )) + + # Manage user tool (admin only) + self.register_tool(MCPTool( + name="manage_users", + description="Manage users", + category=ToolCategory.ADMIN, + parameters={ + "type": "object", + "properties": { + "action": { + "type": "string", + "enum": ["list", "create", "update", "deactivate"], + "description": "Action to perform" + }, + "user_data": { + "type": "object", + "description": "User data (create/update)" + }, + "user_id": { + "type": "string", + "description": "User ID (update/deactivate)" + } + }, + "required": ["action"] + }, + required_permissions=["manage:users"], + examples=[ + { + "action": "list" + }, + { + "action": "create", + "user_data": { + "username": "newuser", + "email": "newuser@example.com", + "role": "user" + } + } + ] + )) + + def register_tool(self, tool: MCPTool): + """Register tool""" + self.tools[tool.name] = tool + + def get_tool(self, tool_name: str) -> Optional[MCPTool]: + """Get tool""" + return self.tools.get(tool_name) + + def list_tools(self, user_permissions: List[str] = None) -> List[MCPTool]: + """List tools by user permissions""" + if user_permissions is None: + return list(self.tools.values()) + + available_tools = [] + for tool in self.tools.values(): + # Check permissions + if all(perm in user_permissions for perm in tool.required_permissions): + available_tools.append(tool) + + return available_tools + + def get_tools_by_category(self, category: ToolCategory, user_permissions: List[str] = None) -> List[MCPTool]: + """List tools by category""" + tools = self.list_tools(user_permissions) + return [tool for tool in tools if tool.category == category] + +# Global tool registry +tool_registry = ToolRegistry() +``` + +### MCP 服务器实现(`src/mcp/server.py`) + +```python +from typing import Dict, Any, List, Optional +from fastapi import HTTPException, status +import asyncio +import json + +from src.mcp.tools import tool_registry, ToolCategory +from src.auth.models import User, Permission +from src.api.routes.items import ItemCRUD +from src.auth.models import user_db + +class MCPServer: + """Model Context Protocol server""" + + def __init__(self): + self.item_crud = ItemCRUD() + self.active_sessions: Dict[str, Dict[str, Any]] = {} + + async def create_session(self, user: User) -> str: + """Create MCP session""" + import uuid + + session_id = str(uuid.uuid4()) + self.active_sessions[session_id] = { + "user_id": user.id, + "user": user, + "created_at": datetime.utcnow(), + "context": {}, + "tool_usage_count": 0, + "last_activity": datetime.utcnow() + } + + return session_id + + async def get_session(self, session_id: str) -> Optional[Dict[str, Any]]: + """Get session""" + session = self.active_sessions.get(session_id) + if session: + session["last_activity"] = datetime.utcnow() + return session + + async def close_session(self, session_id: str): + """Close session""" + if session_id in self.active_sessions: + del self.active_sessions[session_id] + + async def list_tools(self, user: User) -> List[Dict[str, Any]]: + """List tools available to user""" + user_permissions = [perm.value for perm in user.permissions] + tools = tool_registry.list_tools(user_permissions) + + return [ + { + "name": tool.name, + "description": tool.description, + "category": tool.category, + "parameters": tool.parameters, + "examples": tool.examples + } + for tool in tools + ] + + async def execute_tool( + self, + tool_name: str, + parameters: Dict[str, Any], + user: User, + session_id: Optional[str] = None + ) -> Dict[str, Any]: + """Execute tool""" + + # Check if tool exists + tool = tool_registry.get_tool(tool_name) + if not tool: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=f"Tool '{tool_name}' not found" + ) + + # Check permissions + user_permissions = [perm.value for perm in user.permissions] + for required_perm in tool.required_permissions: + if required_perm not in user_permissions: + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail=f"Permission '{required_perm}' required for tool '{tool_name}'" + ) + + # Update session + if session_id: + session = await self.get_session(session_id) + if session: + session["tool_usage_count"] += 1 + + # Execute tool + try: + result = await self._execute_tool_logic(tool_name, parameters, user) + + return { + "success": True, + "tool": tool_name, + "result": result, + "timestamp": datetime.utcnow().isoformat() + } + + except Exception as e: + return { + "success": False, + "tool": tool_name, + "error": str(e), + "timestamp": datetime.utcnow().isoformat() + } + + async def _execute_tool_logic( + self, + tool_name: str, + parameters: Dict[str, Any], + user: User + ) -> Any: + """Execute tool logic""" + + if tool_name == "create_item": + return await self._create_item(parameters) + + elif tool_name == "search_items": + return await self._search_items(parameters) + + elif tool_name == "analyze_items": + return await self._analyze_items(parameters) + + elif tool_name == "manage_users": + return await self._manage_users(parameters, user) + + else: + raise ValueError(f"Tool '{tool_name}' implementation not found") + + async def _create_item(self, parameters: Dict[str, Any]) -> Dict[str, Any]: + """Create item tool implementation""" + from src.schemas.items import ItemCreate + + try: + item_create = ItemCreate(**parameters) + created_item = await self.item_crud.create(item_create) + + return { + "action": "create_item", + "item": created_item.dict(), + "message": f"Item '{created_item.name}' created successfully" + } + except Exception as e: + raise ValueError(f"Failed to create item: {str(e)}") + + async def _search_items(self, parameters: Dict[str, Any]) -> Dict[str, Any]: + """Search item tool implementation""" + query = parameters.get("query", "") + category = parameters.get("category") + min_price = parameters.get("min_price") + max_price = parameters.get("max_price") + limit = parameters.get("limit", 10) + + # Search logic implementation + all_items = await self.item_crud.get_all() + filtered_items = [] + + for item in all_items: + # Text search + if query.lower() not in item.name.lower() and query.lower() not in (item.description or "").lower(): + continue + + # Category filter + if category and getattr(item, 'category', None) != category: + continue + + # Price filter + if min_price is not None and item.price < min_price: + continue + if max_price is not None and item.price > max_price: + continue + + filtered_items.append(item) + + # Result limit + result_items = filtered_items[:limit] + + return { + "action": "search_items", + "query": query, + "total_found": len(filtered_items), + "returned_count": len(result_items), + "items": [item.dict() for item in result_items] + } + + async def _analyze_items(self, parameters: Dict[str, Any]) -> Dict[str, Any]: + """Analyze item tool implementation""" + analysis_type = parameters.get("analysis_type") + date_range = parameters.get("date_range", {}) + + all_items = await self.item_crud.get_all() + + if analysis_type == "price_distribution": + prices = [item.price for item in all_items] + if not prices: + return {"analysis": "price_distribution", "result": "No items found"} + + return { + "analysis": "price_distribution", + "result": { + "total_items": len(prices), + "min_price": min(prices), + "max_price": max(prices), + "average_price": sum(prices) / len(prices), + "price_ranges": { + "under_100k": len([p for p in prices if p < 100000]), + "100k_to_500k": len([p for p in prices if 100000 <= p < 500000]), + "500k_to_1m": len([p for p in prices if 500000 <= p < 1000000]), + "over_1m": len([p for p in prices if p >= 1000000]) + } + } + } + + elif analysis_type == "category_breakdown": + categories = {} + for item in all_items: + category = getattr(item, 'category', 'uncategorized') + categories[category] = categories.get(category, 0) + 1 + + return { + "analysis": "category_breakdown", + "result": { + "total_categories": len(categories), + "categories": categories + } + } + + else: + raise ValueError(f"Unknown analysis type: {analysis_type}") + + async def _manage_users(self, parameters: Dict[str, Any], requesting_user: User) -> Dict[str, Any]: + """Manage user tool implementation""" + action = parameters.get("action") + + # Check admin permissions + if Permission.MANAGE_USERS not in requesting_user.permissions: + raise ValueError("Insufficient permissions for user management") + + if action == "list": + users = [User(**user.dict()) for user in user_db.users.values()] + return { + "action": "list_users", + "total_users": len(users), + "users": [user.dict() for user in users] + } + + elif action == "create": + user_data = parameters.get("user_data", {}) + from src.auth.models import UserCreate + + user_create = UserCreate(**user_data) + created_user = user_db.create_user(user_create) + + return { + "action": "create_user", + "user": User(**created_user.dict()).dict(), + "message": f"User '{created_user.username}' created successfully" + } + + else: + raise ValueError(f"Unknown user management action: {action}") + +# Global MCP server instance +mcp_server = MCPServer() +``` + +## 第 6 步:实现 MCP API 端点 + +### MCP API 路由(`src/api/routes/mcp.py`) + +```python +from typing import Dict, Any, Optional +from fastapi import APIRouter, Depends, HTTPException, status, BackgroundTasks +from pydantic import BaseModel + +from src.auth.dependencies import get_current_user_flexible, RequireMCPTools +from src.auth.models import User +from src.mcp.server import mcp_server +from src.mcp.tools import ToolCategory + +router = APIRouter(prefix="/mcp", tags=["MCP"]) + +class ToolExecuteRequest(BaseModel): + """Tool execution request""" + tool_name: str + parameters: Dict[str, Any] + session_id: Optional[str] = None + +class SessionCreateResponse(BaseModel): + """Session creation response""" + session_id: str + message: str + +@router.post("/session", response_model=SessionCreateResponse) +async def create_mcp_session( + current_user: User = Depends(RequireMCPTools) +): + """Create MCP session""" + session_id = await mcp_server.create_session(current_user) + + return SessionCreateResponse( + session_id=session_id, + message=f"MCP session created (User: {current_user.username})" + ) + +@router.delete("/session/{session_id}") +async def close_mcp_session( + session_id: str, + current_user: User = Depends(RequireMCPTools) +): + """Close MCP session""" + session = await mcp_server.get_session(session_id) + + if not session: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Session not found" + ) + + # Check session owner + if session["user_id"] != current_user.id: + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail="Cannot close another user's session" + ) + + await mcp_server.close_session(session_id) + + return {"message": "Session closed successfully"} + +@router.get("/tools") +async def list_mcp_tools( + category: Optional[ToolCategory] = None, + current_user: User = Depends(RequireMCPTools) +): + """List available MCP tools""" + tools = await mcp_server.list_tools(current_user) + + if category: + tools = [tool for tool in tools if tool["category"] == category] + + return { + "user": current_user.username, + "total_tools": len(tools), + "tools": tools + } + +@router.post("/execute") +async def execute_mcp_tool( + request: ToolExecuteRequest, + background_tasks: BackgroundTasks, + current_user: User = Depends(RequireMCPTools) +): + """Execute MCP tool""" + + # Check session (optional) + if request.session_id: + session = await mcp_server.get_session(request.session_id) + if not session: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Session not found" + ) + + if session["user_id"] != current_user.id: + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail="Cannot use another user's session" + ) + + # Execute tool + result = await mcp_server.execute_tool( + tool_name=request.tool_name, + parameters=request.parameters, + user=current_user, + session_id=request.session_id + ) + + # Log tool usage in background + background_tasks.add_task( + log_tool_usage, + current_user.id, + request.tool_name, + result["success"] + ) + + return result + +@router.get("/sessions") +async def list_user_sessions( + current_user: User = Depends(RequireMCPTools) +): + """List active user sessions""" + user_sessions = [] + + for session_id, session_data in mcp_server.active_sessions.items(): + if session_data["user_id"] == current_user.id: + user_sessions.append({ + "session_id": session_id, + "created_at": session_data["created_at"], + "tool_usage_count": session_data["tool_usage_count"], + "last_activity": session_data["last_activity"] + }) + + return { + "user": current_user.username, + "active_sessions": len(user_sessions), + "sessions": user_sessions + } + +@router.get("/stats") +async def get_mcp_stats( + current_user: User = Depends(RequireMCPTools) +): + """MCP usage statistics""" + total_sessions = len(mcp_server.active_sessions) + user_sessions = len([ + s for s in mcp_server.active_sessions.values() + if s["user_id"] == current_user.id + ]) + + return { + "user_stats": { + "username": current_user.username, + "active_sessions": user_sessions, + "permissions": [perm.value for perm in current_user.permissions] + }, + "server_stats": { + "total_active_sessions": total_sessions, + "available_tools": len(await mcp_server.list_tools(current_user)) + } + } + +async def log_tool_usage(user_id: str, tool_name: str, success: bool): + """Log tool usage (background job)""" + import logging + + logger = logging.getLogger("mcp.usage") + logger.info( + f"Tool usage - User: {user_id}, Tool: {tool_name}, Success: {success}" + ) +``` + +## 第 7 步:应用集成与测试 + +### 主应用(`src/main.py`) + +```python +from fastapi import FastAPI +from fastapi.middleware.cors import CORSMiddleware + +from src.auth.routes import router as auth_router +from src.api.routes.items import router as items_router +from src.api.routes.mcp import router as mcp_router +from src.core.config import settings + +app = FastAPI( + title="AI Integrated API", + description="AI model integrated MCP-based API server", + version="1.0.0" +) + +# CORS settings +app.add_middleware( + CORSMiddleware, + allow_origins=settings.ALLOWED_HOSTS, + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + +# Include routers +app.include_router(auth_router) +app.include_router(items_router, prefix="/api/v1") +app.include_router(mcp_router, prefix="/api/v1") + +@app.get("/") +async def root(): + return { + "message": "AI Integrated API with MCP Support", + "version": "1.0.0", + "endpoints": { + "authentication": "/auth", + "items": "/api/v1/items", + "mcp": "/api/v1/mcp", + "docs": "/docs" + } + } + +@app.get("/health") +async def health_check(): + """Health check endpoint""" + return { + "status": "healthy", + "version": "1.0.0", + "services": { + "auth": "operational", + "mcp": "operational", + "database": "operational" + } + } +``` + +### 启动服务器并测试 + +
+ +```console +$ cd ai-integrated-api +$ fastkit runserver +Starting FastAPI server at 127.0.0.1:8000... + +# User login +$ curl -X POST "http://localhost:8000/auth/login" \ + -H "Content-Type: application/x-www-form-urlencoded" \ + -d "username=admin&password=admin123" + +{ + "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", + "refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", + "token_type": "bearer", + "expires_in": 1800, + "user": { + "id": "123e4567-e89b-12d3-a456-426614174000", + "email": "admin@example.com", + "username": "admin", + "role": "admin", + "permissions": ["read:items", "write:items", ...] + } +} + +# Create MCP session +$ curl -X POST "http://localhost:8000/api/v1/mcp/session" \ + -H "Authorization: Bearer YOUR_ACCESS_TOKEN" + +{ + "session_id": "abc123-def456-ghi789", + "message": "MCP session created (User: admin)" +} + +# List available tools +$ curl "http://localhost:8000/api/v1/mcp/tools" \ + -H "Authorization: Bearer YOUR_ACCESS_TOKEN" + +{ + "user": "admin", + "total_tools": 4, + "tools": [ + { + "name": "create_item", + "description": "Create a new item", + "category": "data_management", + "parameters": {...}, + "examples": [...] + }, + ... + ] +} + +# Execute MCP tool (create item) +$ curl -X POST "http://localhost:8000/api/v1/mcp/execute" \ + -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ + -H "Content-Type: application/json" \ + -d '{ + "tool_name": "create_item", + "parameters": { + "name": "AI generated item", + "description": "MCP through AI generated item", + "price": 500000, + "category": "ai_generated" + }, + "session_id": "abc123-def456-ghi789" + }' + +{ + "success": true, + "tool": "create_item", + "result": { + "action": "create_item", + "item": { + "id": 1, + "name": "AI generated item", + "description": "MCP through AI generated item", + "price": 500000, + "category": "ai_generated", + "created_at": "2024-01-01T12:00:00Z" + }, + "message": "Item 'AI generated item' created successfully" + }, + "timestamp": "2024-01-01T12:00:00.123456Z" +} + +# Execute MCP tool (search item) +$ curl -X POST "http://localhost:8000/api/v1/mcp/execute" \ + -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ + -H "Content-Type: application/json" \ + -d '{ + "tool_name": "search_items", + "parameters": { + "query": "AI", + "limit": 5 + } + }' +``` + +
+ +## 第 8 步:AI 客户端示例 + +### Python MCP 客户端示例 + +```python +# client_example.py +import asyncio +import aiohttp +from typing import Dict, Any, List + +class MCPClient: + """MCP client example""" + + def __init__(self, base_url: str, api_key: str): + self.base_url = base_url + self.api_key = api_key + self.session_id = None + self.session = None + + async def __aenter__(self): + self.session = aiohttp.ClientSession( + headers={"X-API-Key": self.api_key} + ) + return self + + async def __aexit__(self, exc_type, exc_val, exc_tb): + if self.session_id: + await self.close_session() + if self.session: + await self.session.close() + + async def create_session(self) -> str: + """Create MCP session""" + async with self.session.post(f"{self.base_url}/api/v1/mcp/session") as resp: + data = await resp.json() + self.session_id = data["session_id"] + return self.session_id + + async def close_session(self): + """Close MCP session""" + if self.session_id: + async with self.session.delete(f"{self.base_url}/api/v1/mcp/session/{self.session_id}"): + pass + self.session_id = None + + async def list_tools(self) -> List[Dict[str, Any]]: + """List available tools""" + async with self.session.get(f"{self.base_url}/api/v1/mcp/tools") as resp: + data = await resp.json() + return data["tools"] + + async def execute_tool(self, tool_name: str, parameters: Dict[str, Any]) -> Dict[str, Any]: + """Execute tool""" + payload = { + "tool_name": tool_name, + "parameters": parameters, + "session_id": self.session_id + } + + async with self.session.post( + f"{self.base_url}/api/v1/mcp/execute", + json=payload + ) as resp: + return await resp.json() + + async def ai_assistant_workflow(self, user_request: str) -> str: + """模拟 AI 助手工作流""" + + # 1. 创建会话 + await self.create_session() + print(f"Session created: {self.session_id}") + + # 2. 分析用户请求并选择合适的工具 + if "Create item" in user_request or "Create" in user_request: + # 处理创建 item 的请求 + result = await self.execute_tool("create_item", { + "name": "AI recommended item", + "description": "AI generated item based on user request", + "price": 100000, + "category": "ai_recommended" + }) + + if result["success"]: + item_name = result["result"]["item"]["name"] + return f"✅ '{item_name}' item created successfully!" + else: + return f"❌ Item creation failed: {result.get('error', 'Unknown error')}" + + elif "Search" in user_request or "Find" in user_request: + # Search request + search_query = "Item" # Actually extracted from NLP + result = await self.execute_tool("search_items", { + "query": search_query, + "limit": 5 + }) + + if result["success"]: + items = result["result"]["items"] + item_list = "\n".join([f"- {item['name']} (₩{item['price']:,})" for item in items]) + return f"🔍 Search results ({len(items)} items):\n{item_list}" + else: + return f"❌ Search failed: {result.get('error', 'Unknown error')}" + + elif "Analyze" in user_request: + # Analyze request + result = await self.execute_tool("analyze_items", { + "analysis_type": "price_distribution" + }) + + if result["success"]: + analysis = result["result"]["result"] + return f"📊 Price analysis:\nAverage price: ₩{analysis['average_price']:,.0f}\nMinimum: ₩{analysis['min_price']:,} - Maximum: ₩{analysis['max_price']:,}" + else: + return f"❌ Analysis failed: {result.get('error', 'Unknown error')}" + + else: + return "Sorry, I couldn't find a tool to handle that request." + +async def main(): + """Client test""" + async with MCPClient("http://localhost:8000", "your-api-key-here") as client: + + # List available tools + tools = await client.list_tools() + print(f"Available tools: {len(tools)}") + for tool in tools: + print(f"- {tool['name']}: {tool['description']}") + + print("\n" + "="*50 + "\n") + + # AI assistant simulation + test_requests = [ + "Create a new item", + "Search for items", + "Analyze price distribution" + ] + + for request in test_requests: + print(f"User request: {request}") + response = await client.ai_assistant_workflow(request) + print(f"AI response: {response}") + print("-" * 30) + +if __name__ == "__main__": + asyncio.run(main()) +``` + + + + + +## 小结 + +在本教程中,我们实现了 MCP(Model Context Protocol)集成,包括: + +- ✅ 构建基于 JWT 的认证体系 +- ✅ 实现基于角色的访问控制(RBAC) +- ✅ 实现 MCP 服务器与工具系统 +- ✅ 基于会话的上下文管理 +- ✅ 与 AI 模型间的安全 API 通信 +- ✅ 工具权限管理与使用追踪 +- ✅ 实际可用的 AI 客户端示例 + +现在您可以构建一个完整的基于 MCP 的系统,让 AI 模型能够安全、高效地使用 API 能力! diff --git a/docs/zh/user-guide/adding-routes.md b/docs/zh/user-guide/adding-routes.md new file mode 100644 index 0000000..e54880c --- /dev/null +++ b/docs/zh/user-guide/adding-routes.md @@ -0,0 +1,581 @@ +# 添加路由 + +学习如何向已有的 FastAPI 项目添加新的 API 路由。 + +## 基本的路由添加 + +### 使用 `addroute` 命令 + +FastAPI-fastkit 的 `addroute` 命令让添加新路由变得很简单: + +
+ +```console +$ fastkit addroute users my-awesome-api + Adding New Route +┌──────────────────┬──────────────────────────────────────────┐ +│ Project │ my-awesome-api │ +│ Route Name │ users │ +│ Target Directory │ ~/my-awesome-api │ +└──────────────────┴──────────────────────────────────────────┘ + +Do you want to add route 'users' to project 'my-awesome-api'? [Y/n]: y + +╭──────────────────────── Info ────────────────────────╮ +│ ℹ Updated main.py to include the API router │ +╰──────────────────────────────────────────────────────╯ +╭─────────────────────── Success ───────────────────────╮ +│ ✨ Successfully added new route 'users' to project │ +│ `my-awesome-api` │ +╰───────────────────────────────────────────────────────╯ +``` + +
+ +## 会生成什么 + +当您添加一个路由时,FastAPI-fastkit 会自动创建: + +### 1. 路由文件:`src/api/routes/users.py` + +```python +from typing import List +from fastapi import APIRouter, HTTPException, status +from src.schemas.users import User, UserCreate, UserUpdate +from src.crud.users import users_crud + +router = APIRouter() + +@router.get("/", response_model=List[User]) +def read_users(): + """获取所有用户""" + return users_crud.get_all() + +@router.post("/", response_model=User, status_code=status.HTTP_201_CREATED) +def create_user(user: UserCreate): + """创建新用户""" + return users_crud.create(user) + +@router.get("/{user_id}", response_model=User) +def read_user(user_id: int): + """获取指定用户""" + user = users_crud.get_by_id(user_id) + if user is None: + raise HTTPException(status_code=404, detail="User not found") + return user + +@router.put("/{user_id}", response_model=User) +def update_user(user_id: int, user: UserUpdate): + """更新用户""" + updated_user = users_crud.update(user_id, user) + if updated_user is None: + raise HTTPException(status_code=404, detail="User not found") + return updated_user + +@router.delete("/{user_id}", status_code=status.HTTP_204_NO_CONTENT) +def delete_user(user_id: int): + """删除用户""" + success = users_crud.delete(user_id) + if not success: + raise HTTPException(status_code=404, detail="User not found") +``` + +### 2. CRUD 操作:`src/crud/users.py` + +```python +from typing import List, Optional +from src.schemas.users import User, UserCreate, UserUpdate + +class UsersCRUD: + def __init__(self): + self._users: List[User] = [] + self._next_id = 1 + + def get_all(self) -> List[User]: + """获取所有用户""" + return self._users + + def get_by_id(self, user_id: int) -> Optional[User]: + """根据 ID 获取用户""" + return next((user for user in self._users if user.id == user_id), None) + + def create(self, user: UserCreate) -> User: + """创建新用户""" + new_user = User( + id=self._next_id, + title=user.title, + description=user.description + ) + self._next_id += 1 + self._users.append(new_user) + return new_user + + def update(self, user_id: int, user: UserUpdate) -> Optional[User]: + """更新已有用户""" + existing_user = self.get_by_id(user_id) + if existing_user: + update_data = user.dict(exclude_unset=True) + for field, value in update_data.items(): + setattr(existing_user, field, value) + return existing_user + return None + + def delete(self, user_id: int) -> bool: + """删除用户""" + user = self.get_by_id(user_id) + if user: + self._users.remove(user) + return True + return False + +users_crud = UsersCRUD() +``` + +### 3. Pydantic 模式:`src/schemas/users.py` + +```python +from typing import Optional +from pydantic import BaseModel + +class UserBase(BaseModel): + title: str + description: Optional[str] = None + +class UserCreate(UserBase): + pass + +class UserUpdate(BaseModel): + title: Optional[str] = None + description: Optional[str] = None + +class User(UserBase): + id: int + + class Config: + from_attributes = True +``` + +### 4. 注册路由 + +该命令会自动更新 `src/api/api.py`,把新路由器纳入其中: + +```python +from fastapi import APIRouter +from src.api.routes import items, users + +api_router = APIRouter() + +api_router.include_router(items.router, prefix="/items", tags=["items"]) +api_router.include_router(users.router, prefix="/users", tags=["users"]) +``` + +## 生成的 API 端点 + +添加 `users` 路由后,您将拥有以下端点: + +| 方法 | 端点 | 描述 | +|--------|----------|-------------| +| `GET` | `/api/v1/users/` | 获取所有用户 | +| `POST` | `/api/v1/users/` | 创建新用户 | +| `GET` | `/api/v1/users/{user_id}` | 获取指定用户 | +| `PUT` | `/api/v1/users/{user_id}` | 更新用户 | +| `DELETE` | `/api/v1/users/{user_id}` | 删除用户 | + +## 测试新路由 + +### 1. 启动服务器 + +
+ +```console +$ fastkit runserver +INFO: Uvicorn running on http://127.0.0.1:8000 +``` + +
+ +### 2. 查看 API 文档 + +访问 [http://127.0.0.1:8000/docs](http://127.0.0.1:8000/docs),在交互式文档中查看新端点。 + +### 3. 使用 curl 测试 + +**创建用户:** +
+ +```console +$ curl -X POST "http://127.0.0.1:8000/api/v1/users/" \ + -H "Content-Type: application/json" \ + -d '{"title": "John Doe", "description": "Software Developer"}' + +{ + "id": 1, + "title": "John Doe", + "description": "Software Developer" +} +``` + +
+ +**获取所有用户:** +
+ +```console +$ curl http://127.0.0.1:8000/api/v1/users/ + +[ + { + "id": 1, + "title": "John Doe", + "description": "Software Developer" + } +] +``` + +
+ +**获取指定用户:** +
+ +```console +$ curl http://127.0.0.1:8000/api/v1/users/1 + +{ + "id": 1, + "title": "John Doe", + "description": "Software Developer" +} +``` + +
+ +## 定制生成的代码 + +生成出来的代码可以完全按需定制。下面是一些常见的扩展方式: + +### 1. 扩展的用户模式 + +修改 `src/schemas/users.py`,加入更贴近真实场景的字段: + +```python +from typing import Optional +from datetime import datetime +from pydantic import BaseModel, EmailStr, Field + +class UserBase(BaseModel): + email: EmailStr + username: str = Field(..., min_length=3, max_length=50) + full_name: Optional[str] = None + is_active: bool = True + +class UserCreate(UserBase): + password: str = Field(..., min_length=8) + +class UserUpdate(BaseModel): + email: Optional[EmailStr] = None + username: Optional[str] = Field(None, min_length=3, max_length=50) + full_name: Optional[str] = None + is_active: Optional[bool] = None + +class User(UserBase): + id: int + created_at: datetime + + class Config: + from_attributes = True + +class UserInDB(User): + hashed_password: str +``` + +### 2. 增强 CRUD,加入校验 + +更新 `src/crud/users.py`,加入更完善的校验: + +```python +from typing import List, Optional +from datetime import datetime +import hashlib +from src.schemas.users import UserCreate, UserUpdate, UserInDB + +class UsersCRUD: + def __init__(self): + self._users: List[UserInDB] = [] + self._next_id = 1 + + def _hash_password(self, password: str) -> str: + """简单的密码哈希示例(生产环境请使用 bcrypt)""" + return hashlib.sha256(password.encode()).hexdigest() + + def get_by_email(self, email: str) -> Optional[UserInDB]: + """根据邮箱获取用户""" + return next((user for user in self._users if user.email == email), None) + + def get_by_username(self, username: str) -> Optional[UserInDB]: + """根据用户名获取用户""" + return next((user for user in self._users if user.username == username), None) + + def create(self, user: UserCreate) -> UserInDB: + """带校验地创建新用户""" + # 检查是否存在重复用户 + if self.get_by_email(user.email): + raise ValueError("Email already registered") + if self.get_by_username(user.username): + raise ValueError("Username already taken") + + new_user = UserInDB( + id=self._next_id, + email=user.email, + username=user.username, + full_name=user.full_name, + is_active=user.is_active, + created_at=datetime.now(), + hashed_password=self._hash_password(user.password) + ) + self._next_id += 1 + self._users.append(new_user) + return new_user + +users_crud = UsersCRUD() +``` + +### 3. 加入错误处理的路由 + +更新 `src/api/routes/users.py`,加入更完善的错误处理: + +```python +from typing import List +from fastapi import APIRouter, HTTPException, status +from src.schemas.users import User, UserCreate, UserUpdate +from src.crud.users import users_crud + +router = APIRouter() + +@router.post("/", response_model=User, status_code=status.HTTP_201_CREATED) +def create_user(user: UserCreate): + """创建新用户""" + try: + new_user = users_crud.create(user) + # 返回时去掉密码哈希 + return User(**new_user.dict()) + except ValueError as e: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=str(e) + ) + +@router.get("/{user_id}", response_model=User) +def read_user(user_id: int): + """获取指定用户""" + user = users_crud.get_by_id(user_id) + if not user: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=f"User with id {user_id} not found" + ) + return User(**user.dict()) +``` + +## 同时添加多个路由 + +您可以连续添加多个路由,构建一个完整的 API: + +
+ +```console +# 添加更多资源路由(先写路由名,再写项目目录) +$ fastkit addroute products my-awesome-api +$ fastkit addroute orders my-awesome-api +$ fastkit addroute categories my-awesome-api + +# 每条命令都会生成一整套 CRUD 结构 +``` + +
+ +由此可获得一个更完整的 API: + +- `/api/v1/users/` —— 用户管理 +- `/api/v1/products/` —— 商品目录 +- `/api/v1/orders/` —— 订单处理 +- `/api/v1/categories/` —— 类目管理 + +## 路由组织 + +### 按相关性分组端点 + +可按领域组织路由: + +```python +# src/api/api.py +from fastapi import APIRouter +from src.api.routes import users, products, orders, categories + +api_router = APIRouter() + +# 用户管理 +api_router.include_router( + users.router, + prefix="/users", + tags=["User Management"] +) + +# 电商相关 +api_router.include_router( + products.router, + prefix="/products", + tags=["E-commerce"] +) +api_router.include_router( + orders.router, + prefix="/orders", + tags=["E-commerce"] +) +api_router.include_router( + categories.router, + prefix="/categories", + tags=["E-commerce"] +) +``` + +### 为路由添加依赖 + +为路由添加认证或其他依赖: + +```python +from fastapi import APIRouter, Depends +from src.core.auth import get_current_user + +router = APIRouter() + +@router.get("/profile", response_model=User) +def get_user_profile(current_user: User = Depends(get_current_user)): + """获取当前用户的个人资料""" + return current_user + +@router.post("/", response_model=User) +def create_user( + user: UserCreate, + current_user: User = Depends(get_current_user) +): + """创建新用户(仅限管理员)""" + if not current_user.is_admin: + raise HTTPException(status_code=403, detail="Admin access required") + return users_crud.create(user) +``` + +## 最佳实践 + +### 1. 命名一致 + +遵循一致的命名约定: + +- **路由名称**:使用复数名词(`users`、`products`、`orders`) +- **模式名称**:使用单数(`User`、`Product`、`Order`) +- **CRUD 类**:以 `CRUD` 结尾(`UsersCRUD`、`ProductsCRUD`) + +### 2. 错误处理 + +始终优雅地处理错误: + +```python +@router.post("/", response_model=User) +def create_user(user: UserCreate): + try: + return users_crud.create(user) + except ValueError as e: + raise HTTPException(status_code=400, detail=str(e)) + except Exception as e: + raise HTTPException(status_code=500, detail="Internal server error") +``` + +### 3. 文档 + +建议为接口补充清晰的 docstring: + +```python +@router.get("/{user_id}", response_model=User) +def read_user(user_id: int): + """ + 根据 ID 获取指定用户。 + + Args: + user_id:用户的唯一标识 + + Returns: + User:包含完整信息的用户对象 + + Raises: + HTTPException:当用户不存在时返回 404 + """ + user = users_crud.get_by_id(user_id) + if not user: + raise HTTPException(status_code=404, detail="User not found") + return user +``` + +### 4. 测试 + +始终为新路由编写测试: + +```python +# tests/test_users.py +from fastapi.testclient import TestClient +from src.main import app + +client = TestClient(app) + +def test_create_user(): + user_data = { + "email": "test@example.com", + "username": "testuser", + "password": "securepassword123" + } + response = client.post("/api/v1/users/", json=user_data) + assert response.status_code == 201 + assert response.json()["email"] == user_data["email"] + +def test_get_user(): + response = client.get("/api/v1/users/1") + assert response.status_code == 200 +``` + +## 故障排查 + +### 路由未出现 + +如果您的路由没有出现在 API 文档中: + +1. **检查路由器是否在 `src/api/api.py` 中注册** +2. **添加路由后重启服务器** +3. **检查路由文件中的导入错误** + +### 导入错误 + +如果出现导入错误: + +1. **核对文件结构是否符合预期布局** +2. **检查路由文件与 CRUD 文件中的模式导入** +3. **确保所有 `__init__.py` 文件都存在** + +### 服务器无法启动 + +如果添加路由后服务器无法启动: + +1. **检查生成文件是否存在语法错误** +2. **检查文件之间的模式是否兼容** +3. **查看日志获取具体的错误信息** + +## 下一步 + +学会添加路由后: + +1. **[您的第一个项目](../tutorial/first-project.md)**:构建完整的博客 API +2. **[CLI 参考](cli-reference.md)**:学习所有可用命令 +3. **[使用模板](using-templates.md)**:探索预构建的项目模板 + +!!! tip "路由开发小贴士" + - 始终在交互式文档(`/docs`)中测试新路由 + - 使用合适的 HTTP 状态码 + - 为所有端点实现完善的错误处理 + - 让路由处理器保持简单,把业务逻辑委托给 CRUD 类 diff --git a/docs/zh/user-guide/choosing-a-starter.md b/docs/zh/user-guide/choosing-a-starter.md new file mode 100644 index 0000000..29b3a46 --- /dev/null +++ b/docs/zh/user-guide/choosing-a-starter.md @@ -0,0 +1,145 @@ +# 应该选择哪个 starter? + +FastAPI-fastkit 提供了多种项目起步方式。本页是一份**面向新手的选型指南**:先在这里决定路线,再跳转到 [快速上手](quick-start.md) 实际创建项目。 + +如果您拿不定主意,最简短的建议是: + +> **从 `fastkit init --interactive` 开始,并选择 `domain-starter` 预设。** 它是现代 API 项目的推荐默认选项。 + +下面会进一步解释为什么这样推荐,以及在哪些情况下更适合选择其他方案。 + +## TL;DR —— 按用户类型选择 + +| 您的情况 | 起点 | +|---|---| +| 刚接触 FastAPI,想要引导式上手 | `fastkit init --interactive`(预设:**`domain-starter`**) | +| 想要一个可运行的 CRUD 示例来阅读与修改 | `fastkit startdemo fastapi-default` | +| 想要尽可能小的骨架 | `fastkit init --interactive`(预设:**`minimal`**) | +| 编写快速原型 / 单文件脚本 | `fastkit init --interactive`(预设:**`single-module`**) | +| 需要真实的数据库(PostgreSQL + SQLAlchemy + Alembic) | `fastkit startdemo fastapi-psql-orm` | +| 想要面向中型 API 的生产风格领域布局 | `fastkit init --interactive`(预设:**`domain-starter`**) | + +## `startdemo` 与 `init --interactive` —— 有什么区别? + +这是两个主要入口,分别面向不同的使用需求。 + +### `fastkit startdemo