diff --git a/docs/de/contributing/development-setup.md b/docs/de/contributing/development-setup.md index f14cef6..9f63f89 100644 --- a/docs/de/contributing/development-setup.md +++ b/docs/de/contributing/development-setup.md @@ -212,6 +212,27 @@ Das Projekt-Makefile bietet praktische Befehle für gängige Entwicklungsaufgabe | `make inspect-templates-verbose` | Mit ausführlicher Ausgabe inspizieren | | `make inspect-template` | Eine oder mehrere Vorlagen inspizieren (Parameter TEMPLATES) | +Das zugrunde liegende Skript kennt drei Flags, die beim Iterieren nützlich sind: `--offline` (überspringt die PyPI-Aktualitätsabfragen), `--no-smoke` (überspringt das Starten des generierten Projekts mit uvicorn — der langsamste Schritt) und `--mypy` (Typprüfung des generierten Projekts; optional, weil sie langsam ist). Was jede Prüfung genau tut, beschreibt [Qualitätssicherung der Vorlagen](../reference/template-quality-assurance.md). + +```console +$ python scripts/inspect-templates.py --templates fastapi-sqlmodel --offline --no-smoke +``` + +### Generierter Code liegt in Jinja2-Fragmenten + +Alles, was `fastkit init --interactive` in ein Projekt schreibt — `main.py`, Datenbank- und Auth-Module, Docker-Dateien, die pytest-Konfiguration — wird aus Vorlagen unter `src/fastapi_fastkit/fragments/` gerendert und nicht in Python aus Strings zusammengesetzt. `main.py.j2` codiert keine Funktion fest: Es iteriert über die Listen `import_fragments`, `lifespan_fragments` und `setup_fragments`, die `DynamicConfigGenerator` liefert, und bindet jedes Fragment ein, sodass sich Funktionen frei kombinieren lassen. + +Eine Funktion hinzuzufügen, sieht ungefähr so aus: + +1. Legen Sie die Bausteine in `main/imports/`, `main/lifespan/` und/oder `main/setup/` ab (Lifespan-Fragmente sind um vier Leerzeichen eingerückt, die anderen beiden nicht). +2. Registrieren Sie sie in `DynamicConfigGenerator._main_fragments()`, abgesichert durch die passende Konfigurationsabfrage. +3. Für ein eigenständiges Modul fügen Sie `/.py.j2` hinzu und rendern es mit `self._render(...)`. +4. Erweitern Sie die Kombinationsmatrix in `tests/test_backends/test_project_builder_config_generator.py` — sie prüft, dass sich jede gerenderte Python-Datei mit `ast.parse` parsen lässt. + +Zwei Einschränkungen, über die man leicht stolpert: Jedes Fragment muss mit einem abschließenden Zeilenumbruch enden (die Umgebung wird mit `keep_trailing_newline=True` erstellt), und `main.py.j2` muss seine Anker `# fastkit:imports` / `# fastkit:routes` behalten — `fastkit addroute` fügt an ihnen ein. + +Das maßgebliche Vorgehen ist direkt neben den Fragmenten dokumentiert, in [`src/fastapi_fastkit/fragments/README.md`](https://github.com/bnbong/FastAPI-fastkit/blob/main/src/fastapi_fastkit/fragments/README.md). + ### Dokumentations-Befehle | Befehl | Beschreibung | @@ -353,13 +374,16 @@ FastAPI-fastkit/ - **`src/fastapi_fastkit/`** — Hauptquellcode des Pakets - **`cli.py`** — Haupteinstiegspunkt des CLI - **`backend/`** — zentrale Backend-Logik - - **`inspector.py`** — Vorlagen-Inspektor + - **`inspection/`** — Pipeline zur Vorlagen-Inspektion (context, checks, consistency, freshness, smoke, lint, report) + - **`inspector.py`** — Fassade über `inspection/`, beibehalten für den historischen Importpfad - **`interactive/`** — Komponenten des interaktiven Modus (Prompts, Selectors, Validators) - **`package_managers/`** — Implementierungen der Paketmanager (pip, uv, pdm, poetry) - - **`project_builder/`** — Hilfsfunktionen zum Projektbau + - **`project_builder/`** — Hilfsfunktionen zum Projektbau, einschließlich des fragmentbasierten Konfigurationsgenerators + - **`scaffolder.py`** — die Generierungspipeline, die `init` und `startdemo` gemeinsam nutzen - **`transducer.py`** — Vorlagen-Transducer - **`core/`** — zentrale Konfiguration und Exceptions - - **`fastapi_project_template/`** — Projektvorlagen (fastapi-default, fastapi-async-crud usw.) + - **`fragments/`** — Jinja2-Fragmente, die in generierte Projekte gerendert werden (siehe oben) + - **`fastapi_project_template/`** — Projektvorlagen (fastapi-default, fastapi-domain-starter usw.) - **`utils/`** — gemeinsame Hilfsfunktionen - **`tests/`** — Test-Suite - **`test_backends/`** — Backend-spezifische Tests diff --git a/docs/de/contributing/template-creation-guide.md b/docs/de/contributing/template-creation-guide.md index 0c51521..2f4d14b 100644 --- a/docs/de/contributing/template-creation-guide.md +++ b/docs/de/contributing/template-creation-guide.md @@ -97,8 +97,7 @@ fastapi-{template-name}/ │ ├── 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) +├── pyproject.toml-tpl # ✅ Primary metadata (PEP 621, required) ├── requirements.txt-tpl # 🟡 Optional when pyproject declares deps ├── setup.cfg-tpl # Development tools configuration ├── README.md-tpl # ✅ Project documentation (required) @@ -110,10 +109,16 @@ fastapi-{template-name}/ - ein `tests/`-Verzeichnis - eine `README.md-tpl`-Datei -- mindestens eine Metadaten-Datei: `pyproject.toml-tpl` (bevorzugt, PEP 621) oder `setup.py-tpl` (legacy, weiterhin akzeptiert) -- eine Deklaration von `fastapi` als Abhängigkeit in mindestens einem von: `pyproject.toml-tpl` `[project].dependencies`, `requirements.txt-tpl` oder `setup.py-tpl` `install_requires` +- `pyproject.toml-tpl` (PEP 621) +- eine Deklaration von `fastapi` als Abhängigkeit in `pyproject.toml-tpl` `[project].dependencies` oder `requirements.txt-tpl` -`requirements.txt-tpl` ist nicht mehr zwingend erforderlich, wenn `pyproject.toml-tpl` `[project].dependencies` deklariert. Moderne Vorlagen SOLLTEN `pyproject.toml-tpl` als primäre Metadaten-Datei adoptieren. +`requirements.txt-tpl` ist nicht erforderlich, wenn `pyproject.toml-tpl` `[project].dependencies` deklariert — liefert eine Vorlage jedoch beide Dateien mit, müssen sie übereinstimmen: Die Inspektion schlägt bei Abhängigkeitsabweichungen zwischen ihnen fehl. + +**Neue Vorlagen sind pyproject-first und dürfen keine `setup.py-tpl` mitliefern.** Seit v1.4.0 enthält keine mitgelieferte Vorlage mehr eine solche Datei. Die Inspektion akzeptiert die Datei weiterhin, damit externe Vorlagen funktionsfähig bleiben, aber es handelt sich um einen Legacy-Pfad. + +**Zielen Sie durchgängig auf Python 3.12**: `requires-python = ">=3.12"`, black `target-version = ["py312"]`, mypy `python_version = "3.12"` und `python:3.12-slim` für jedes Docker-Basis-Image. Die Inspektion prüft, dass die drei Festlegungen übereinstimmen. + +**Deklarieren Sie `FastAPI-fastkit` niemals als Laufzeitabhängigkeit** eines generierten Projekts. Das CLI ist ein Entwicklungswerkzeug; die Abhängigkeitsliste einer Vorlage sollte nur enthalten, was die generierte Anwendung selbst importiert. ### Anleitung zum Schreiben der Dateien @@ -237,52 +242,23 @@ isort==5.12.0 mypy==1.7.1 ``` -#### 4. setup.py-tpl schreiben (legacy — optional, falls pyproject vorhanden) +#### 4. Routen-Anker -Erhalten für Legacy-Vorlagen. Neue Vorlagen benötigen diese Datei nicht, wenn sie `pyproject.toml-tpl` mitliefern. +Vorlagen, die eine `main.py-tpl` oder ein API-Aggregatormodul mitliefern, sollten markieren, wo `fastkit addroute` Code einfügt: ```python -""" - package setup +from src.app.api import health -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", -] +# fastkit:imports -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", - ], -) +api_router = APIRouter() +api_router.include_router(health.router) + +# fastkit:routes ``` +Ohne die Anker greift `addroute` auf ein AST-basiertes Einfügen zurück, das den Importblock und die Router-Registrierungen selbst ermittelt — eine Vorlage ohne Anker funktioniert also weiterhin, das Ergebnis ist aber weniger vorhersehbar. Lassen Sie die Kommentare beim Bearbeiten dieser Dateien an ihrem Platz. + #### 5. Test-Dateien schreiben ```python @@ -342,8 +318,17 @@ make inspect-template TEMPLATES="fastapi-your-template" # Validate with verbose output python scripts/inspect-templates.py --templates "fastapi-your-template" --verbose + +# Fast local loop: no network, no server boot +python scripts/inspect-templates.py --templates "fastapi-your-template" \ + --offline --no-smoke + +# Full check before opening a PR (adds mypy over the generated project) +python scripts/inspect-templates.py --templates "fastapi-your-template" --mypy ``` +`--offline` überspringt die PyPI-Abfragen zur Aktualität der Abhängigkeiten, `--no-smoke` überspringt das Starten des generierten Projekts (der langsamste Schritt), und `--mypy` führt eine Typprüfung des generierten Projekts durch. Was jede Prüfung tut, beschreibt [Qualitätssicherung der Vorlagen](../reference/template-quality-assurance.md). + !!! note Wenn Sie eine PR einreichen, läuft der **Template PR Inspection**-Workflow automatisch und validiert Ihre Vorlagenänderungen. Sie erhalten das Feedback direkt in Ihrer PR. @@ -356,7 +341,7 @@ Der Inspektor validiert automatisch die folgenden Punkte: - [ ] `tests/`-Verzeichnis existiert - [ ] `README.md-tpl`-Datei existiert -- [ ] Mindestens eine von `pyproject.toml-tpl` (bevorzugt) oder `setup.py-tpl` (legacy) existiert +- [ ] `pyproject.toml-tpl` existiert (keine `setup.py-tpl` in neuen Vorlagen) #### ✅ Validierung der Dateiendungen @@ -368,7 +353,8 @@ Der Inspektor validiert automatisch die folgenden Punkte: - [ ] `fastapi` ist in mindestens einem von Folgendem deklariert: - [ ] `pyproject.toml-tpl` unter `[project].dependencies` (bevorzugt) - [ ] `requirements.txt-tpl` - - [ ] `setup.py-tpl` unter `install_requires` +- [ ] `requirements.txt-tpl` und `[project].dependencies` widersprechen sich nicht +- [ ] `FastAPI-fastkit` ist **keine** Laufzeitabhängigkeit #### ✅ Validierung der FastAPI-Implementierung @@ -381,6 +367,24 @@ Der Inspektor validiert automatisch die folgenden Punkte: - [ ] Installation der Abhängigkeiten erfolgreich - [ ] Alle pytest-Tests bestehen +#### ✅ Smoke-Test + +- [ ] Das generierte Projekt startet unter uvicorn +- [ ] `/docs` antwortet +- [ ] `/health` antwortet + +Das uvicorn-Ziel wird aus `[tool.fastapi-fastkit].app_module` in der generierten `pyproject.toml` ermittelt, danach aus `app_module` in `template-config.yml` und schließlich aus den üblichen `main.py`-Speicherorten. + +#### ✅ Konsistenz der Konfiguration + +- [ ] `requires-python`, black `target-version` und mypy `python_version` geben alle Python 3.12 an +- [ ] Keine Abhängigkeitsabweichung zwischen `requirements.txt-tpl` und `[project].dependencies` +- [ ] Keine Selbstabhängigkeit von `FastAPI-fastkit` + +#### ✅ Platzhalter-Rückstände + +- [ ] Kein `` übersteht die Projektgenerierung + #### ✅ Automatisiertes Vorlagen-Testen FastAPI-fastkit enthält **automatisierte Vorlagen-Tests**, die umfassende Tests für alle Vorlagen ausführen: diff --git a/docs/de/index.md b/docs/de/index.md index 3de4713..50ec32d 100644 --- a/docs/de/index.md +++ b/docs/de/index.md @@ -34,6 +34,8 @@ Inspiriert wurde dieses Projekt vom `SpringBoot initializer` und vom CLI-Werkzeu - **🔍 Automatisierte Qualitätssicherung der Vorlagen**: wöchentliche automatisierte Tests stellen sicher, dass alle Vorlagen funktionsfähig und aktuell bleiben - **🚀 Mehrere Projektvorlagen**: Auswahl aus verschiedenen vorkonfigurierten Vorlagen für unterschiedliche Anwendungsfälle (async CRUD, Docker, PostgreSQL usw.) - **📦 Unterstützung mehrerer Paketmanager**: wählen Sie Ihren bevorzugten Python-Paketmanager (pip, uv, pdm, poetry) für die Verwaltung der Abhängigkeiten +- **🧾 Reproduzierbare Projektkonfigurationen**: speichern Sie eine interaktive Sitzung mit `--save-config` und spielen Sie sie später mit `fastkit init --config` erneut ab — ohne Eingabeaufforderungen, mit demselben Projekt +- **🏷️ Selbstbeschreibende Projekte**: jedes generierte Projekt hält seine Vorlage, sein Preset, seinen Paketmanager, seinen Einstiegspunkt und die gewählten Funktionen in einem `[tool.fastapi-fastkit]`-Block fest, den `runserver` und `addroute` wieder auslesen ## Installation @@ -123,7 +125,7 @@ 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 │ +│ ℹ Injected metadata into pyproject.toml │ ╰──────────────────────────────────────────────────────╯ ╭──────────────────────── Info ────────────────────────╮ │ ℹ Injected metadata into config file │ @@ -174,6 +176,16 @@ Installing dependencies... Dieser Befehl erstellt eine neue FastAPI-Projekt-Workspace-Umgebung mit einer virtuellen Python-Umgebung. +- Wichtige Optionen: + - `--project-name`, `--author`, `--author-email`, `--description` + - `--package-manager` [pip|uv|pdm|poetry] + - `--config `: das Projekt ohne Eingabeaufforderungen aus einer gespeicherten Konfigurationsdatei erstellen (`.json` / `.toml` sowie `.yaml`, wenn PyYAML installiert ist) + - `--save-config `: zusammen mit `--interactive` die Antworten zur späteren Wiederverwendung aufzeichnen + - `--dry-run`: die Dateien und Pakete ausgeben, die erstellt würden, ohne etwas zu schreiben + - `--no-venv`: Erstellung der virtuellen Umgebung überspringen (impliziert `--no-install`) + - `--no-install`: Installation der Abhängigkeiten überspringen + - `--yes` / `-y`: beim Bereitstellen im aktuellen Verzeichnis die Bestätigung „Overwrite these files?“ überspringen + ### Ein Projekt im interaktiven Modus erstellen ✨ NEU! Für komplexere Projekte nutzen Sie den **interaktiven Modus**, um Ihre FastAPI-Anwendung Schritt für Schritt mit intelligenter Funktionsauswahl aufzubauen: @@ -264,6 +276,30 @@ Select utilities (comma-separated numbers, e.g., 1,3,4): Select utilities: 1 +🪵 Logging Format +Select logging format: + 1. structured - JSON logs + X-Request-ID correlation middleware (stdlib only) + 2. None - Uvicorn's default plain-text logging + +Select logging format [2]: 1 + +🧬 Database Migrations +Select migration tool: + 1. Alembic - alembic.ini + async alembic/env.py + scripts/migrate.sh (recommended for SQL databases) + 2. None - No migration tooling + +Select migration tool [1]: 1 + +🧰 Developer Tooling +Select tooling to generate (comma-separated numbers, e.g., 1,3) + 1. ruff ruff + ruff-format config in pyproject.toml (replaces black/isort) + 2. pre-commit .pre-commit-config.yaml + 3. github-actions .github/workflows/test.yml (Python 3.12) + 4. devcontainer .devcontainer/devcontainer.json + 5. makefile Makefile with install/test/lint/format/run + +Your choice (or press Enter to skip): 1,2 + 🚀 Deployment Configuration Select deployment option: 1. Docker - Generate Dockerfile @@ -294,6 +330,9 @@ Enter custom package names (comma-separated, press Enter to skip): │ Monitoring │ Prometheus │ │ Testing │ Coverage │ │ Utilities │ CORS │ +│ Logging │ structured │ +│ Migrations │ Alembic │ +│ Tooling │ ruff, pre-commit │ │ Package Manager │ uv │ └─────────────────────┴───────────────────────────────────────────────────────────────────────────┘ @@ -350,6 +389,16 @@ Der interaktive Modus bietet: - **Intelligente Verwaltung der Abhängigkeiten** mit automatischer pip-Kompatibilität - **Funktionsvalidierung** mit Hinweisen auf manuelle Verkabelung für Auswahlmöglichkeiten, die das Preset nicht automatisch verdrahten kann - **Identitätsmarker** in der generierten `pyproject.toml` (Beschreibungs-Marker + `[tool.fastapi-fastkit]`-Tabelle), damit `is_fastkit_project()` generierte Projekte später wiedererkennt +- **Projektmetadaten** in `[tool.fastapi-fastkit]` — Vorlage, Preset, Paketmanager, `app_module` und die gewählten `features` —, die `fastkit runserver` und `fastkit addroute` wieder auslesen +- **Reproduzierbare Läufe**: `--save-config ` zeichnet die Antworten auf, `--config ` spielt sie ohne eine einzige Eingabeaufforderung erneut ab, und `--dry-run` zeigt eine Vorschau des Ergebnisses, ohne etwas zu schreiben + +Jede Auswahl im Katalog erzeugt inzwischen echten, funktionierenden Code, statt nur ein Paket zu installieren — Hintergrundaufgaben (Celery/Dramatiq), Redis-Caching, WebSocket, Pagination, OpenTelemetry-Tracing sowie OAuth2- und sessionbasierte Authentifizierung werden alle mit einem passenden Modul und der Verdrahtung in `main.py` ausgeliefert. Drei neue Achsen runden den Katalog ab: + +- **Logging**: `structured` ergänzt strukturierte Ausgabe mit stdlib `logging` + `json` und eine Request-ID-Middleware — ohne neue Abhängigkeit +- **Migrationen**: `Alembic` erzeugt für jede SQL-Datenbankauswahl eine vollständige asynchrone Migrationsumgebung (`alembic.ini`, `alembic/env.py`, eine Basis-Revision, `scripts/migrate.sh`) +- **Tooling** (Mehrfachauswahl): `ruff`, `pre-commit`, `github-actions`, `devcontainer`, `makefile` — jedes erzeugt seine eigene Konfigurationsdatei; `ruff` fügt außerdem einen `[tool.ruff]`-Block in `pyproject.toml` ein + +Jedes generierte Projekt erhält außerdem die Endpunkte `/health` und `/ready`, und `--dry-run` listet genau dieselbe Dateimenge auf, die ein echter Lauf schreiben würde, einschließlich dieser neuen Artefakte. `classic-layered` und `domain-starter` behalten ihre mitgelieferte `main.py`, daher warnt die Funktionsvalidierung, wenn eine Auswahl in einer dieser Achsen manuell verdrahtet werden muss — Details finden Sie in der [Preset- / Funktionsmatrix](reference/preset-feature-matrix.md). ### Eine neue Route zum FastAPI-Projekt hinzufügen @@ -445,6 +494,12 @@ FastAPI template project will deploy at '~your-project-path~' +- Wichtige Optionen: + - `--project-name`, `--author`, `--author-email`, `--description` + - `--package-manager` [pip|uv|pdm|poetry] + - `--dry-run`, `--no-venv`, `--no-install`, `--yes`/`-y` — gleiche Bedeutung wie bei `init` + - Verweigert die Ausführung, wenn das Zielprojektverzeichnis bereits existiert (außer mit `--dry-run`) + Um die Liste der verfügbaren FastAPI-Demos anzuzeigen, prüfen Sie mit:
@@ -463,18 +518,42 @@ $ fastkit list-templates │ │ PostgreSQL │ │ fastapi-default │ Simple FastAPI Project │ │ fastapi-single-module │ FastAPI Single Module Template │ +│ fastapi-auth-jwt │ FastAPI JWT Authentication │ +│ fastapi-sqlmodel │ FastAPI SQLModel │ +│ fastapi-llm-agent │ FastAPI LLM Agent │ └────────────────────────┴───────────────────────────────────────────────────────┘ ```
+Verfügbare Vorlagen: + +| Vorlage | Beschreibung | Hinweise | +|---|---|---| +| [`fastapi-default`](https://github.com/bnbong/FastAPI-fastkit/blob/main/src/fastapi_fastkit/fastapi_project_template/fastapi-default/README.md-tpl) | Einfaches FastAPI-Projekt mit klassischem Schichten-Layout | Gute erste Wahl | +| [`fastapi-empty`](https://github.com/bnbong/FastAPI-fastkit/blob/main/src/fastapi_fastkit/fastapi_project_template/fastapi-empty/README.md-tpl) | Minimale FastAPI-Vorlage | Basis für das Preset `minimal` | +| [`fastapi-single-module`](https://github.com/bnbong/FastAPI-fastkit/blob/main/src/fastapi_fastkit/fastapi_project_template/fastapi-single-module/README.md-tpl) | FastAPI-App in einer Datei | Basis für das Preset `single-module` | +| [`fastapi-domain-starter`](https://github.com/bnbong/FastAPI-fastkit/blob/main/src/fastapi_fastkit/fastapi_project_template/fastapi-domain-starter/README.md-tpl) | Domänenorientierter, pyproject-first Starter für mittelgroße APIs | Basis für das Preset `domain-starter` | +| [`fastapi-auth-jwt`](https://github.com/bnbong/FastAPI-fastkit/blob/main/src/fastapi_fastkit/fastapi_project_template/fastapi-auth-jwt/README.md-tpl) | JWT-Authentifizierung mit Refresh-Token-Rotation, argon2-Hashing und Scopes | Neu in v1.4.0 | +| [`fastapi-sqlmodel`](https://github.com/bnbong/FastAPI-fastkit/blob/main/src/fastapi_fastkit/fastapi_project_template/fastapi-sqlmodel/README.md-tpl) | Asynchrones SQLModel + Alembic-Migrationen + generisches CRUD mit Paginierung | Neu in v1.4.0 | +| [`fastapi-llm-agent`](https://github.com/bnbong/FastAPI-fastkit/blob/main/src/fastapi_fastkit/fastapi_project_template/fastapi-llm-agent/README.md-tpl) | Streaming-Claude-Chat-Agent (SSE) mit Tool-Call-Schleife | Neu in v1.4.0 | +| [`fastapi-mcp`](https://github.com/bnbong/FastAPI-fastkit/blob/main/src/fastapi_fastkit/fastapi_project_template/fastapi-mcp/README.md-tpl) | FastAPI-App, die als MCP-Server bereitgestellt wird | | +| [`fastapi-psql-orm`](https://github.com/bnbong/FastAPI-fastkit/blob/main/src/fastapi_fastkit/fastapi_project_template/fastapi-psql-orm/README.md-tpl) | Item-Management-API mit PostgreSQL, SQLModel, Alembic und docker-compose | Erfordert Docker | +| [`fastapi-custom-response`](https://github.com/bnbong/FastAPI-fastkit/blob/main/src/fastapi_fastkit/fastapi_project_template/fastapi-custom-response/README.md-tpl) | Item-Management-API mit eigener Antwort-Hülle, Fehlerbehandlung und Paginierung | | +| [`fastapi-async-crud`](https://github.com/bnbong/FastAPI-fastkit/blob/main/src/fastapi_fastkit/fastapi_project_template/fastapi-async-crud/README.md-tpl) | Asynchrone Item-Management-API | **Veraltet** — verwenden Sie `fastapi-sqlmodel` | +| [`fastapi-dockerized`](https://github.com/bnbong/FastAPI-fastkit/blob/main/src/fastapi_fastkit/fastapi_project_template/fastapi-dockerized/README.md-tpl) | Dockerisierte Item-Management-API | **Veraltet** — verwenden Sie `fastapi-default` mit der Docker-Option in `init --interactive` | + +Veraltete Vorlagen funktionieren weiterhin, werden aber nicht mehr als Ausgangspunkt empfohlen. Eine Entscheidungshilfe finden Sie unter [Welchen Starter soll ich wählen?](user-guide/choosing-a-starter.md). + ## Dokumentation Für umfassende Anleitungen und detaillierte Nutzungshinweise erkunden Sie unsere Dokumentation: +- 🧭 **[Welchen Starter soll ich wählen?](user-guide/choosing-a-starter.md)** — Entscheidungshilfe für Einsteiger zu `startdemo`-Vorlagen und interaktiven Presets - 📚 **[Benutzerhandbuch](user-guide/quick-start.md)** — ausführliche Installations- und Nutzungsanleitungen - 🎯 **[Tutorial](tutorial/getting-started.md)** — Schritt-für-Schritt-Tutorials für Einsteiger - 📖 **[CLI-Referenz](user-guide/cli-reference.md)** — vollständige Befehlsreferenz +- 🧱 **[Architektur-Preset-Matrix](reference/preset-feature-matrix.md)** — Vertrag je Preset und je Funktion für die interaktive Generierung - 🔍 **[Qualitätssicherung der Vorlagen](reference/template-quality-assurance.md)** — automatisierte Tests und Qualitätsstandards ## 🚀 Vorlagenbasierte Tutorials @@ -486,16 +565,19 @@ Lernen Sie die FastAPI-Entwicklung anhand praktischer Anwendungsfälle mit unser - **[Einen einfachen API-Server bauen](tutorial/basic-api-server.md)** — erstellen Sie Ihren ersten FastAPI-Server mit der Vorlage `fastapi-default` - **[Eine asynchrone CRUD-API bauen](tutorial/async-crud-api.md)** — entwickeln Sie eine leistungsstarke asynchrone API mit der Vorlage `fastapi-async-crud` - **[Domänenorientiertes Projekt (Domain Starter)](tutorial/domain-starter.md)** — bauen Sie eine mittelgroße API mit der Vorlage `fastapi-domain-starter`, dem empfohlenen modernen Standard +- **[JWT-Authentifizierung](tutorial/auth-jwt.md)** — fügen Sie mit der Vorlage `fastapi-auth-jwt` echte Konten hinzu: rotierende Refresh-Tokens, argon2id-Hashing, Rollen- und Scope-Guards ### 🗄️ Datenbank und Infrastruktur - **[Integration einer Datenbank](tutorial/database-integration.md)** — nutzen Sie PostgreSQL + SQLAlchemy mit der Vorlage `fastapi-psql-orm` -- **[Docker-Containerisierung und Deployment](tutorial/docker-deployment.md)** — richten Sie eine produktionsreife Deployment-Umgebung mit der Vorlage `fastapi-dockerized` ein +- **[Asynchrone Persistenz mit SQLModel](tutorial/sqlmodel.md)** — asynchrones SQLModel, Alembic-Migrationen, eine generische CRUD-Basis und paginierte Listen mit der Vorlage `fastapi-sqlmodel` +- **[Docker-Containerisierung und Deployment](tutorial/docker-deployment.md)** — richten Sie eine produktionsreife Deployment-Umgebung mit der Vorlage `fastapi-dockerized` ein *(veraltete Vorlage — für bestehende Nutzer beibehalten)* ### ⚡ Erweiterte Funktionen - **[Benutzerdefinierte Antwortbehandlung und fortgeschrittenes API-Design](tutorial/custom-response-handling.md)** — bauen Sie Enterprise-APIs mit der Vorlage `fastapi-custom-response` - **[Integration mit MCP](tutorial/mcp-integration.md)** — erstellen Sie einen API-Server, der mit KI-Modellen integriert ist, mit der Vorlage `fastapi-mcp` +- **[Streaming-LLM-Agent](tutorial/llm-agent.md)** — bauen Sie mit der Vorlage `fastapi-llm-agent` ein streamendes Claude-Chat-Backend mit einer echten Tool-Call-Schleife Jedes Tutorial bietet: diff --git a/docs/de/reference/preset-feature-matrix.md b/docs/de/reference/preset-feature-matrix.md index f1424bc..e226f54 100644 --- a/docs/de/reference/preset-feature-matrix.md +++ b/docs/de/reference/preset-feature-matrix.md @@ -29,12 +29,18 @@ Diese Funktionen werden in **jedem** Preset unterstützt — die Paketinstallati | Funktion | `minimal` / `single-module` | `classic-layered` / `domain-starter` | |---|---|---| | **Datenbank** (PostgreSQL, MySQL, SQLite, MongoDB) | Erzeugt das Konfigurationsmodul **und** legt `await init_db()`-Aufrufe in der regenerierten `main.py` an. | Erzeugt das Konfigurationsmodul am Pfad des Presets. Die mitgelieferte `main.py` bleibt **erhalten**, daher `get_db()` manuell in Router einbinden. | -| **Authentifizierung** (JWT, FastAPI-Users, OAuth2, sessionbasiert) | Erzeugt das Auth-Konfigurationsmodul. JWT importiert zusätzlich `HTTPBearer` in der regenerierten `main.py`. | Erzeugt das Auth-Konfigurationsmodul am Pfad des Presets. Keine Imports werden zu `main.py` hinzugefügt — Abhängigkeiten manuell verdrahten. | -| **Hintergrundaufgaben** (Celery, Dramatiq) | Pakete installiert; aktuell kein main.py-Overlay. | Wie nebenan. | -| **Caching** (Redis) | Pakete installiert; aktuell kein main.py-Overlay. | Wie nebenan. | +| **Authentifizierung** (JWT, FastAPI-Users, OAuth2, sessionbasiert) | Erzeugt das Auth-Konfigurationsmodul; JWT importiert `HTTPBearer`, OAuth2 und sessionbasiert ergänzen zusätzlich ihr Middleware-Setup in der `main.py`. | Erzeugt das Auth-Konfigurationsmodul am Pfad des Presets. Keine Imports werden zu `main.py` hinzugefügt — Abhängigkeiten manuell verdrahten. | +| **Hintergrundaufgaben** (Celery, Dramatiq) | Erzeugt `worker.py` und `features/tasks.py` und bindet den Task-Router in der regenerierten `main.py` ein. | Erzeugt `worker.py` und `features/tasks.py`, aber die mitgelieferte `main.py` bleibt **erhalten** — binden Sie den Router manuell ein. | +| **Caching** (Redis) | Erzeugt `features/cache.py` und bindet es in der regenerierten `main.py` ein bzw. initialisiert es dort. | Erzeugt `features/cache.py`; binden Sie es manuell ein. | +| **Utilities: Pagination / WebSocket** | Erzeugt `features/pagination.py` / `features/websocket.py` und bindet den Router in der regenerierten `main.py` ein. | Erzeugt dasselbe Modul; binden Sie es manuell ein. | +| **Monitoring: OpenTelemetry** | Keine eigene Datei — das Tracing-Setup wird direkt in die regenerierte `main.py` eingefügt. | Keine eigene Datei. Die mitgelieferte `main.py` bleibt erhalten, daher ist das Tracing **nicht verdrahtet** — ergänzen Sie das Setup manuell. | +| **Migrationen** (Alembic) | Erzeugt `alembic.ini`, `alembic/env.py`, eine Basis-Revision und `scripts/migrate.sh` für jede SQL-Datenbankauswahl — unabhängig vom `main.py`-Overlay. | Wie nebenan — die Alembic-Generierung hängt nicht davon ab, ob `main.py` erhalten bleibt. | +| **Tooling** (ruff, pre-commit, github-actions, devcontainer, makefile) | Erzeugt die passende(n) Konfigurationsdatei(en), unabhängig vom `main.py`-Overlay. | Wie nebenan. | +| **Logging** (strukturiert) | Erzeugt `logging_config.py` und verdrahtet die Request-ID-Middleware in der regenerierten `main.py`. | Erzeugt `logging_config.py`; die mitgelieferte `main.py` bleibt erhalten, daher die Middleware manuell verdrahten. | | **CORS** (Utility) | `CORSMiddleware` mit `allow_origins=['*']` wird in die regenerierte `main.py` eingefügt. | **Bereits verdrahtet** in der mitgelieferten `main.py` (bedingt durch `settings.all_cors_origins`). Aktivieren Sie es, indem Sie `BACKEND_CORS_ORIGINS` in `.env` setzen — keine Code-Änderungen nötig. | -| **Testen** (Basic / Coverage / Advanced) | `pytest.ini` wird im Projekt-Root erzeugt. | Wie nebenan. | +| **Testen** (Basic / Coverage / Advanced) | `pytest.ini` wird im Projekt-Root erzeugt; Advanced fügt zusätzlich `tests/factories.py` und `tests/test_factories.py` hinzu. | Wie nebenan. | | **Deployment** (Docker, docker-compose) | `Dockerfile` und/oder `docker-compose.yml` werden im Projekt-Root abgelegt. | Wie nebenan. | +| **`/health` / `/ready`** | Immer in der regenerierten `main.py` vorhanden. | Bereits in der erhaltenen, vorlagenseitig mitgelieferten `main.py` vorhanden. | ## Wann eine Warnung zur Preset-Kompatibilität erscheint @@ -44,10 +50,15 @@ Bei Presets, die die **mitgelieferte `main.py` erhalten** (`classic-layered`, `d |---|---| | `CORS` (Utility) | ❌ — bereits in der mitgelieferten `main.py` verdrahtet. Setzen Sie nur `BACKEND_CORS_ORIGINS` in `.env`. | | `Rate-Limiting` (Utility) | ✅ — Setup des `slowapi`-Limiters wird nicht ergänzt | -| `Prometheus` (Monitoring) | ✅ — `Instrumentator().instrument(app)` wird nicht aufgerufen | +| `Pagination` / `WebSocket` (Utility) | ✅ — der generierte Router (`features/pagination.py` / `features/websocket.py`) wird nicht eingebunden | +| `Celery` / `Dramatiq` (async_tasks) | ✅ — der generierte `worker.py`- / `features/tasks.py`-Router wird nicht eingebunden | +| `Redis` (Caching) | ✅ — das generierte `features/cache.py` wird weder eingebunden noch initialisiert | +| `Prometheus` / `OpenTelemetry` (Monitoring) | ✅ — das Instrumentierungs-Setup wird nicht zu `main.py` hinzugefügt | +| `structured` (Logging) | ✅ — `logging_config.py` wird erzeugt, aber die Request-ID-Middleware wird nicht zu `main.py` hinzugefügt | +| `Alembic` (Migrationen), `tooling`-Auswahlen | ❌ — diese Dateien werden unabhängig vom Preset erzeugt und sind nutzbar; nichts hängt von `main.py` ab | | Jegliche Datenbank- / Auth-Auswahl | ⚠️ — Konfigurationsdateien werden erzeugt, aber Sie müssen sie per `Depends()` in Ihre Router einbinden | -Für die Presets `minimal` und `single-module` übernimmt das dynamische `main.py`-Overlay CORS, Rate-Limiting und Prometheus-Instrumentierung automatisch; es werden keine Warnungen ausgelöst. +Für die Presets `minimal` und `single-module` übernimmt das dynamische `main.py`-Overlay alle oben genannten Achsen automatisch; es werden keine Warnungen ausgelöst. ## Nicht unterstützte Kombinationen (auf Nummer sicher gehen) diff --git a/docs/de/reference/template-quality-assurance.md b/docs/de/reference/template-quality-assurance.md index 83ce66f..44ff401 100644 --- a/docs/de/reference/template-quality-assurance.md +++ b/docs/de/reference/template-quality-assurance.md @@ -21,6 +21,38 @@ Jeden Mittwoch um Mitternacht (UTC) inspiziert unser GitHub-Actions-Workflow aut - ✅ **Abhängigkeitsprüfung** — bestätigt, dass FastAPI und benötigte Abhängigkeiten ordnungsgemäß deklariert sind - ✅ **FastAPI-Implementierung** — prüft, dass Vorlagen eine korrekte FastAPI-App-Initialisierung enthalten - ✅ **Testausführung** — führt die Tests der Vorlage aus, um die Funktionsfähigkeit zu bestätigen +- ✅ **Smoke-Test** — startet das generierte Projekt mit uvicorn und prüft seine HTTP-Oberfläche +- ✅ **Konsistenz der Konfiguration** — Festlegung der Python-Version, Abhängigkeitsabweichungen, Selbstabhängigkeit +- ✅ **Platzhalter-Rückstände** — kein `` übersteht die Projektgenerierung +- ✅ **Aktualität der Abhängigkeiten** — warnt vor festgelegten Versionen, die hinter ihrem neuesten PyPI-Release zurückliegen + +## Was jede Prüfung tut + +Die Inspektion generiert aus der Vorlage ein **echtes Projekt** — Kopieren, `-tpl`-Konvertierung, Einfügen der Metadaten — und führt dann jede Prüfung gegen dieses generierte Projekt aus, nicht gegen den Quellcode der Vorlage. Eine Prüfung, die nur die Vorlage liest, kann keinen Platzhalter erkennen, den die Generierung nicht ersetzt hat. + +### Smoke-Test + +Das generierte Projekt wird mit uvicorn auf einem freien Port gestartet, und der Inspektor fragt so lange ab, bis der Server antwortet oder das Timeout abläuft. Sobald er läuft, werden `/docs` und `/health` angefragt und ihre Statuscodes geprüft. Anschließend wird der Serverprozess beendet, notfalls mit `SIGKILL`, wenn er nicht stoppt. + +Das uvicorn-Ziel wird in dieser Reihenfolge ermittelt: `[tool.fastapi-fastkit].app_module` in der generierten `pyproject.toml`, dann `app_module` in der `template-config.yml` der Vorlage, dann die üblichen `main.py`-Speicherorte. Das ist derselbe Vertrag, den `fastkit runserver` verwendet: Eine Vorlage, deren Smoke-Test startet, ist eine Vorlage, deren generiertes Projekt läuft. + +Diese Prüfung findet, was statische Analyse nicht finden kann: einen Import, der sich erst zur Laufzeit auflöst, einen `lifespan`, der eine Ausnahme auslöst, ein Konfigurationsfeld ohne Standardwert. + +### Konsistenz der Konfiguration + +Drei Dinge werden gegen die `pyproject.toml-tpl` der Vorlage geprüft: + +- **Die Festlegungen der Python-Version stimmen überein und zielen auf 3.12** — `requires-python`, `target-version` von black und `python_version` von mypy. Eine Vorlage, die als `py311` formatiert, aber `>=3.12` deklariert, ist ein Fehler, der nur darauf wartet, einen Mitwirkenden zu treffen. +- **Keine Abhängigkeitsabweichung** — die in `requirements.txt-tpl` und in `[project].dependencies` deklarierten Pakete dürfen sich nicht widersprechen. Vorlagen liefern beide Dateien aus Gründen der Paketmanager-Kompatibilität mit, und zwei Listen laufen immer auseinander, solange nichts sie prüft. +- **Keine Selbstabhängigkeit** — ein generiertes Projekt darf `FastAPI-fastkit` nicht als Laufzeitabhängigkeit deklarieren. Das CLI ist ein Entwicklungswerkzeug; es in jede generierte App mitzuliefern, zieht den gesamten Abhängigkeitsbaum von fastkit in fremde Projekte. + +### Platzhalter-Rückstände + +Jedes ``, `` und Verwandte müssen während der Generierung ersetzt werden. Jeder ``, der im generierten Projekt noch vorhanden ist, ist ein Fehler — er bedeutet, dass das Einfügen der Metadaten diese Datei nicht kennt und der Nutzer sie unverändert erhalten würde. + +### Aktualität der Abhängigkeiten + +Festgelegte Versionen werden mit dem neuesten Release auf PyPI verglichen und als Warnungen gemeldet, nie als Fehler — eine Vorlage, die eine Minor-Version zurückliegt, ist eine Information, kein Fehlschlag. Dies ist die einzige Prüfung, die auf das Netzwerk zugreift, und `--offline` überspringt sie. ## Automatisiertes Vorlagen-Testsystem @@ -140,6 +172,30 @@ $ python scripts/inspect-templates.py --verbose $ python scripts/inspect-templates.py --output my_results.json ``` +#### Inspektions-Flags + +| Flag | Wirkung | Verwenden Sie es, wenn | +|---|---|---| +| `--offline` | Überspringt jede Netzwerkabfrage (Bericht zur Aktualität der Abhängigkeiten) | kein Netzwerk verfügbar ist oder Sie einen schnellen, abgeschotteten Lauf möchten | +| `--no-smoke` | Überspringt das Starten des generierten Projekts mit uvicorn | Sie nur an der Struktur iterieren; der Smoke-Test ist der langsamste Schritt | +| `--mypy` | Führt eine Typprüfung des generierten Projekts mit mypy durch | Sie eine Vorlage einreichen — optional, weil es langsam ist | +| `--templates` | Kommagetrennte Liste der zu inspizierenden Vorlagen | Sie an einer einzelnen Vorlage arbeiten | +| `--verbose` | Ausführliche Ausgabe | Sie einen Fehlschlag debuggen | +| `--output` | Pfad für die JSON-Ergebnisdatei | Sie mehrere Läufe nebeneinander aufbewahren möchten | + +Eine schnelle lokale Schleife für eine einzelne Vorlage: + +```console +$ python scripts/inspect-templates.py \ + --templates fastapi-sqlmodel --offline --no-smoke --verbose +``` + +…und die vollständige Prüfung vor dem Öffnen einer PR: + +```console +$ python scripts/inspect-templates.py --templates fastapi-sqlmodel --mypy +``` + ### Makefile-Befehle verwenden ```console @@ -192,10 +248,11 @@ Damit eine Vorlage die Inspektion besteht, muss sie diese Anforderungen erfülle - Muss ein `src/`-Verzeichnis mit Python-Quellcodedateien enthalten - Python-Dateien müssen die Endung `.py-tpl` verwenden - Muss ein `tests/`-Verzeichnis und eine `README.md-tpl`-Datei enthalten -- Muss **mindestens eine** Metadaten-Datei enthalten: - - `pyproject.toml-tpl` (bevorzugt, PEP 621), oder - - `setup.py-tpl` (legacy, weiterhin akzeptiert) -- `requirements.txt-tpl` ist optional, wenn `pyproject.toml-tpl` `[project].dependencies` deklariert +- Muss `pyproject.toml-tpl` (PEP 621) enthalten. Mitgelieferte Vorlagen sind pyproject-first und liefern keine `setup.py-tpl` mehr mit; die Inspektion akzeptiert sie weiterhin, damit Vorlagen von Drittanbietern funktionsfähig bleiben, aber neue Vorlagen dürfen keine hinzufügen. +- `requirements.txt-tpl` ist optional, wenn `pyproject.toml-tpl` `[project].dependencies` deklariert — sind jedoch beide vorhanden, müssen sie übereinstimmen (siehe *Konsistenz der Konfiguration*) +- Muss durchgängig auf Python 3.12 zielen: `requires-python = ">=3.12"`, black `target-version = ["py312"]`, mypy `python_version = "3.12"` und `python:3.12-slim` für jedes Docker-Basis-Image +- Darf `FastAPI-fastkit` nicht als Laufzeitabhängigkeit deklarieren +- Darf nach der Generierung keinen `` unersetzt lassen ### FastAPI-Anforderungen - Muss die FastAPI-App-Initialisierung enthalten diff --git a/docs/de/reference/translation-status.md b/docs/de/reference/translation-status.md index 0473b62..51f0f72 100644 --- a/docs/de/reference/translation-status.md +++ b/docs/de/reference/translation-status.md @@ -18,15 +18,15 @@ Die unten stehenden Zahlen zählen die Markdown-Seiten im Verzeichnisbaum jeder | Sprache | Status | Markdown-Seiten | Anmerkungen | |---|---|---:|---| -| 🇬🇧 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`) | ✅ 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`. | +| 🇬🇧 Englisch (`en`) | ✅ Maßgebliche Referenz | 29 / 29 | Maßgeblich. | +| 🇰🇷 Koreanisch (`ko`) | ✅ Vollständig | 29 / 29 | Phasen 1–3 abgeschlossen. Am 2026-10-11 mit den englischen Quellen von v1.4.1 neu abgeglichen: Die drei Tutorials aus v1.4.0 (`tutorial/auth-jwt.md`, `tutorial/sqlmodel.md`, `tutorial/llm-agent.md`) sind übersetzt, und Seiten, die seit ihrer ursprünglichen Übersetzung auseinandergelaufen waren, wurden wieder angeglichen. `docs/ko/changelog.md` verweist bewusst auf das kanonische englische `CHANGELOG.md`. | +| 🇯🇵 Japanisch (`ja`) | ✅ Vollständig | 29 / 29 | Phasen 1–3 abgeschlossen. Am 2026-10-11 mit den englischen Quellen von v1.4.1 neu abgeglichen: Die drei Tutorials aus v1.4.0 (`tutorial/auth-jwt.md`, `tutorial/sqlmodel.md`, `tutorial/llm-agent.md`) sind übersetzt, und Seiten, die seit ihrer ursprünglichen Übersetzung auseinandergelaufen waren, wurden wieder angeglichen. `docs/ja/changelog.md` verweist bewusst auf das kanonische englische `CHANGELOG.md`. | +| 🇨🇳 Chinesisch (`zh`) | ✅ Vollständig | 29 / 29 | Phasen 1–3 abgeschlossen. Am 2026-10-11 mit den englischen Quellen von v1.4.1 neu abgeglichen: Die drei Tutorials aus v1.4.0 (`tutorial/auth-jwt.md`, `tutorial/sqlmodel.md`, `tutorial/llm-agent.md`) sind übersetzt, und Seiten, die seit ihrer ursprünglichen Übersetzung auseinandergelaufen waren, wurden wieder angeglichen. `docs/zh/changelog.md` verweist bewusst auf das kanonische englische `CHANGELOG.md`. | +| 🇪🇸 Spanisch (`es`) | ✅ Vollständig | 29 / 29 | Phasen 1–3 abgeschlossen. Am 2026-10-11 mit den englischen Quellen von v1.4.1 neu abgeglichen: Die drei Tutorials aus v1.4.0 (`tutorial/auth-jwt.md`, `tutorial/sqlmodel.md`, `tutorial/llm-agent.md`) sind übersetzt, und Seiten, die seit ihrer ursprünglichen Übersetzung auseinandergelaufen waren, wurden wieder angeglichen. `docs/es/changelog.md` verweist bewusst auf das kanonische englische `CHANGELOG.md`. | +| 🇫🇷 Französisch (`fr`) | ✅ Vollständig | 29 / 29 | Phasen 1–3 abgeschlossen. Am 2026-10-11 mit den englischen Quellen von v1.4.1 neu abgeglichen: Die drei Tutorials aus v1.4.0 (`tutorial/auth-jwt.md`, `tutorial/sqlmodel.md`, `tutorial/llm-agent.md`) sind übersetzt, und Seiten, die seit ihrer ursprünglichen Übersetzung auseinandergelaufen waren, wurden wieder angeglichen. `docs/fr/changelog.md` verweist bewusst auf das kanonische englische `CHANGELOG.md`. | +| 🇩🇪 Deutsch (`de`) | ✅ Vollständig | 29 / 29 | Phasen 1–3 abgeschlossen. Am 2026-10-11 mit den englischen Quellen von v1.4.1 neu abgeglichen: Die drei Tutorials aus v1.4.0 (`tutorial/auth-jwt.md`, `tutorial/sqlmodel.md`, `tutorial/llm-agent.md`) sind übersetzt, und Seiten, die seit ihrer ursprünglichen Übersetzung auseinandergelaufen waren, wurden wieder angeglichen. `docs/de/changelog.md` verweist bewusst auf das kanonische englische `CHANGELOG.md`. | -*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: +*Stand verifiziert am 2026-10-11, nach dem Release von v1.4.1. Alle sechs übersetzten Sprachversionen wurden mit den englischen Quellen gleichgezogen: Die drei in v1.4.0 hinzugefügten Tutorials wurden übersetzt, und jede Seite, die seit ihrer ursprünglichen Übersetzung auseinandergelaufen war, wurde neu abgeglichen. Übersetzungen können innerhalb eines Releases dennoch hinterherhinken — wenn eine übersetzte Seite der englischen widerspricht, vertrauen Sie der englischen.* 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 @@ -39,6 +39,7 @@ Wenn die Neuzählung nicht mit der Tabelle übereinstimmt, ist die Tabelle veral Legende: - ✅ **Maßgebliche Referenz** — die Sprache, in der zuerst geschrieben wird. +- ✅ **Vollständig** — jede englische Seite hat ein übersetztes Gegenstück. Einzelne Seiten können innerhalb eines Releases dennoch einer englischen Aktualisierung hinterherhinken. - 🟡 **Teilweise** — einige Seiten sind übersetzt; fehlende Seiten fallen auf Englisch zurück. - 🔴 **Skelett** — der Eintrag im Sprachumschalter existiert, es sind aber noch keine übersetzten Seiten eingecheckt. Die Seite rendert englische Inhalte unter den übersetzten Navigationsbeschriftungen. diff --git a/docs/de/tutorial/auth-jwt.md b/docs/de/tutorial/auth-jwt.md new file mode 100644 index 0000000..d7915ca --- /dev/null +++ b/docs/de/tutorial/auth-jwt.md @@ -0,0 +1,249 @@ +# JWT-Authentifizierung mit `fastapi-auth-jwt` + +Bauen Sie eine API, die vom ersten Commit an echte Konten hat. Dieses Tutorial führt Sie vollständig durch die Vorlage `fastapi-auth-jwt`: wie Token-Paare ausgestellt und rotiert werden, warum ein erneut eingespielter Refresh-Token abgelehnt wird, wie die Rollen- und Scope-Guards verdrahtet sind und was sich ändert, wenn Sie SQLite hinter sich lassen. + +## Was Sie lernen werden + +- Ein Projekt mit `fastkit startdemo fastapi-auth-jwt` generieren +- Den Unterschied zwischen Access-Token und Refresh-Token und warum Rotation wichtig ist +- Wie `get_current_user`, `get_current_active_superuser` und `require_scopes(...)` eine Route schützen +- Wo das Passwort-Hashing stattfindet und warum es argon2id ist +- Von der mitgelieferten SQLite-Datei mit Alembic zu PostgreSQL wechseln + +## Voraussetzungen + +- Python 3.12+ +- FastAPI-fastkit installiert (`pip install fastapi-fastkit`) +- Sicherer Umgang mit FastAPI-Dependencies und Pydantic-Schemas + +Wenn Sie noch nie eine FastAPI-App gebaut haben, beginnen Sie mit [Einen einfachen API-Server bauen](basic-api-server.md) und kehren Sie dann zurück — diese Vorlage setzt die Grundlagen voraus. + +## Schritt 1: Das Projekt generieren + +```console +$ fastkit startdemo fastapi-auth-jwt +Enter the project name: accounts-api +Enter the author name: Developer Kim +Enter the author email: developer@example.com +Enter the project description: Accounts and sessions service +Select package manager (pip, uv, pdm, poetry) [uv]: uv +Do you want to proceed with project creation? [y/N]: y +``` + +Starten Sie es anschließend: + +```console +$ cd accounts-api +$ bash scripts/run-server.sh # or: uvicorn src.app.main:app --reload +``` + +Die App startet mit SQLite (`sqlite:///./app.db`) und legt ihre Tabellen beim Start an, sodass Sie vorher nichts installieren oder konfigurieren müssen. Die Dokumentation finden Sie unter . + +!!! tip "Vorschau vor dem Festlegen" + `fastkit startdemo fastapi-auth-jwt --dry-run` gibt den Dateibaum und die Paketliste aus, ohne etwas zu schreiben — praktisch, um zu sehen, was Sie gleich bekommen. + +## Schritt 2: Der generierte Baum + +``` +accounts-api/ +├── pyproject.toml # PEP 621 metadata + [tool.fastapi-fastkit] +├── requirements.txt +├── alembic.ini +├── Dockerfile +├── .env # SECRET_KEY, DATABASE_URL, token lifetimes +├── scripts/ +│ ├── format.sh lint.sh run-server.sh test.sh +├── src/ +│ └── app/ +│ ├── main.py # FastAPI() + lifespan + CORS + api_router +│ ├── core/ +│ │ ├── config.py # pydantic-settings +│ │ └── security.py # argon2 hashing + JWT encode/decode +│ ├── db/session.py # engine + get_session dependency +│ ├── api/ +│ │ ├── router.py # aggregates health + every domain router +│ │ ├── health.py # GET /health +│ │ └── deps.py # session, current user, role/scope guards +│ ├── domains/ +│ │ ├── auth/ # login, refresh rotation, logout +│ │ └── users/ # registration and profiles +│ └── alembic/ # migration environment + versions/ +└── tests/ + ├── conftest.py test_health.py test_auth.py test_users.py +``` + +Das Layout folgt der Form von `domain-starter`: Jede Domäne besitzt ihre eigene `router.py` (Transport), `service.py` (Geschäftslogik), `repository.py` (Datenzugriff), `models.py` (Tabellen) und `schemas.py` (API-Ein-/Ausgabe). Authentifizierungsbelange liegen in `auth/`, Kontobelange in `users/`, und keine der beiden Domänen greift auf die Interna der anderen zu. + +## Schritt 3: Registrieren, anmelden, eine geschützte Route aufrufen + +```console +# 1. register +$ curl -X POST http://127.0.0.1:8000/api/v1/auth/register \ + -H 'Content-Type: application/json' \ + -d '{"email": "alice@example.com", "password": "s3cret-password"}' + +# 2. log in — note the form encoding: this is the OAuth2 password flow +$ curl -X POST http://127.0.0.1:8000/api/v1/auth/login \ + -d 'username=alice@example.com&password=s3cret-password' +{"access_token": "...", "refresh_token": "...", "token_type": "bearer"} + +# 3. call a protected route +$ curl http://127.0.0.1:8000/api/v1/users/me \ + -H "Authorization: Bearer $ACCESS_TOKEN" +``` + +Die Schaltfläche „Authorize“ in Swagger steuert denselben Ablauf, sodass Sie geschützte Routen direkt aus `/docs` heraus ausprobieren können, ohne den Browser zu verlassen. + +Die vollständige Oberfläche: + +| Methode | Endpunkt | Auth | Beschreibung | +|---------|-----------------------------|---------------|--------------------------------------------| +| GET | `/api/v1/health` | — | Liveness-Probe | +| POST | `/api/v1/auth/register` | — | Ein Konto anlegen | +| POST | `/api/v1/auth/login` | — | OAuth2-Passwortformular → Token-Paar | +| POST | `/api/v1/auth/refresh` | Refresh-Token | In ein neues Token-Paar rotieren | +| POST | `/api/v1/auth/logout` | Refresh-Token | Einen Refresh-Token widerrufen | +| POST | `/api/v1/auth/logout-all` | Access-Token | Alle Sitzungen des Benutzers widerrufen | +| GET | `/api/v1/users/me` | Access-Token | Profil des aktuellen Benutzers | +| PATCH | `/api/v1/users/me` | Access-Token | Profil des aktuellen Benutzers aktualisieren | +| GET | `/api/v1/users/me/scopes` | Scope `me` | Beispielroute mit Scope-Prüfung | +| GET | `/api/v1/users` | Superuser | Benutzerliste mit Rollenprüfung | + +## Schritt 4: Zwei Tokens, und warum + +Eine Anmeldung liefert ein **Paar** zurück: + +- Der **Access-Token** ist kurzlebig (`ACCESS_TOKEN_EXPIRE_MINUTES`, standardmäßig 30) und wird bei jeder Anfrage mitgeschickt. Weil er kurzlebig ist, wird ein geleakter Token schnell unbrauchbar. +- Der **Refresh-Token** ist langlebig (`REFRESH_TOKEN_EXPIRE_DAYS`, standardmäßig 14) und dient genau einem Zweck: ein neues Paar zu erhalten. + +Bis hierhin ist nichts ungewöhnlich. Was diese Vorlage von einem durchschnittlichen JWT-Snippet unterscheidet, ist, dass Refresh-Tokens **erfasst und rotiert** werden. Jeder ausgestellte Refresh-Token erhält einen `jti`-Claim, und dieser `jti` wird in die Datenbank geschrieben: + +```python +def issue_token_pair(self, user: User) -> TokenPair: + scopes = user.scope_list + access_token, _, _ = create_access_token(str(user.id), scopes) + refresh_token, jti, expires_at = create_refresh_token(str(user.id), scopes) + self.refresh_tokens.add(jti, user.id or 0, expires_at) + return TokenPair(access_token=access_token, refresh_token=refresh_token) +``` + +Beim Refresh wird der vorgelegte Token widerrufen, **bevor** das neue Paar ausgestellt wird: + +```python +def rotate(self, refresh_token: str) -> TokenPair: + payload = decode_token(refresh_token, REFRESH_TOKEN_TYPE) + record = self.refresh_tokens.get_by_jti(str(payload.get("jti"))) + if record is None or record.revoked: + raise TokenError("refresh token is no longer valid") + ... + self.refresh_tokens.revoke(record) + return self.issue_token_pair(user) +``` + +Probieren Sie es aus: Führen Sie einmal einen Refresh durch und senden Sie dann den *alten* Refresh-Token erneut. + +```console +$ curl -X POST http://127.0.0.1:8000/api/v1/auth/refresh \ + -H 'Content-Type: application/json' \ + -d "{\"refresh_token\": \"$REFRESH_TOKEN\"}" +# → new pair + +$ curl -X POST http://127.0.0.1:8000/api/v1/auth/refresh \ + -H 'Content-Type: application/json' \ + -d "{\"refresh_token\": \"$REFRESH_TOKEN\"}" +# → 401: the token died the moment it was used +``` + +Genau diese eine Eigenschaft macht aus einem gestohlenen Refresh-Token statt einer dauerhaften Hintertür einen erkennbaren, einmaligen Fehlschlag. + +Logout funktioniert auf dieselbe Weise, und das ist die andere Sache, die einfache JWT-Setups normalerweise nicht können: `POST /auth/logout` widerruft einen Token, `POST /auth/logout-all` widerruft alle Refresh-Tokens des Benutzers. + +## Schritt 5: Passwörter + +Das Hashing liegt in `src/app/core/security.py` und läuft über `pwdlib` mit **argon2id** — dem aktuell empfohlenen Standard, nicht einer veralteten bcrypt-Konfiguration. Nichts in den Routern oder Services sieht jemals ein Klartext-Passwort über den Moment der Verifikation hinaus, und `hashed_password` ist nie Teil eines Antwort-Schemas. + +Hier müssen Sie nichts konfigurieren. Das Einzige, was Sie konfigurieren *müssen*, ist der Signaturschlüssel. + +## Schritt 6: Rollen und Scopes + +Drei Guards werden als FastAPI-Dependencies mitgeliefert: + +```python +CurrentUser = Annotated[User, Depends(get_current_user)] +``` + +- **`get_current_user`** dekodiert den Bearer-Access-Token, lädt den Benutzer und weist inaktive Konten mit `403` ab. Alles, was fehlerhaft oder abgelaufen ist oder auf einen gelöschten Benutzer verweist, wird mit `401` beantwortet. +- **`get_current_active_superuser`** legt eine `is_superuser`-Prüfung darüber. `GET /api/v1/users` verwendet ihn. +- **`require_scopes(...)`** erzeugt eine Dependency, die bestimmte Scopes verlangt: + +```python +from fastapi import Depends +from src.app.api.deps import require_scopes + +@router.get("/reports", dependencies=[Depends(require_scopes("reports"))]) +def read_reports() -> list[str]: + ... +``` + +Scopes liegen in der Benutzerzeile als kommagetrennter String (`scopes: str`, ausgelesen über die Property `scope_list`) und werden in jeden ausgestellten Token kopiert. Das ist bewusst die einfachste Lösung, die für einen Starter funktioniert. Wenn Ihr Berechtigungsmodell einem String entwächst, ersetzen Sie die Spalte durch eine Rollentabelle und lassen die Signaturen der Dependencies unverändert — in den Routern muss nichts verschoben werden. + +## Schritt 7: Eine geschützte Domäne hinzufügen + +Um zum Beispiel eine `orders`-Domäne hinzuzufügen, die nur angemeldete Benutzer erreichen können: + +1. Legen Sie `src/app/domains/orders/` nach dem Vorbild von `users/` an (`models.py`, `schemas.py`, `repository.py`, `service.py`, `router.py`). +2. Nehmen Sie `CurrentUser` als Parameter in die Routen auf, die ein Konto benötigen, oder hängen Sie `Depends(require_scopes("orders"))` an den Router. +3. Registrieren Sie den Router in `src/app/api/router.py` oberhalb des Ankers `# fastkit:routes`. `fastkit addroute` schreibt in dieselben Anker, sodass handgeschriebene und generierte Routen an einer Stelle bleiben. +4. Generieren Sie eine Migration: `alembic revision --autogenerate -m "add orders"`. +5. Bilden Sie `tests/test_users.py` für die neuen Endpunkte nach. + +## Schritt 8: Jetzt SQLite, später PostgreSQL + +Die Standard-`DATABASE_URL` ist eine lokale SQLite-Datei, und die App ruft beim Start `create_db_and_tables()` auf. Deshalb funktionieren `pytest` und `uvicorn` ohne jede Einrichtung. Für alles über die lokale Entwicklung hinaus sollten Sie stattdessen Alembic das Schema verwalten lassen: + +```console +$ export DATABASE_URL='postgresql+psycopg://postgres:postgres@localhost:5432/app_db' +$ alembic upgrade head +``` + +Nach einer Modelländerung: + +```console +$ alembic revision --autogenerate -m "add profile fields" +``` + +## Schritt 9: Vor dem Deployment + +Zwei Einstellungen in `.env` entscheiden, ob dies eine Demo oder ein echter Dienst ist: + +- **`SECRET_KEY`** signiert jeden Token. Bleibt er ungesetzt, wird pro Prozess ein zufälliger Schlüssel erzeugt — das meldet bei jedem Neustart alle Benutzer ab und führt dazu, dass mehrere Worker sich uneinig sind, welche Tokens gültig sind. Erzeugen Sie einen Schlüssel und legen Sie ihn fest: + + ```console + $ python -c "import secrets; print(secrets.token_urlsafe(32))" + ``` + +- **`DATABASE_URL`** wählt die Datenbank aus. Der SQLite-Standard ist kein Produktionsspeicher. + +Optional können Sie `FIRST_SUPERUSER_EMAIL` und `FIRST_SUPERUSER_PASSWORD` setzen, damit beim Start ein Superuser angelegt wird, und `BACKEND_CORS_ORIGINS` auf eine kommagetrennte Liste von Frontend-Origins setzen. + +## Schritt 10: Die Tests + +```console +$ bash scripts/test.sh # or: pytest +``` + +Die Testsuite führt `httpx.AsyncClient` gegen eine In-Memory-SQLite-Datenbank aus und deckt den gesamten Erfolgspfad ab — register → login → me → refresh → logout — sowie die Ablehnungspfade, auf die es wirklich ankommt: falsches Passwort, fehlerhafter Token, abgelaufener Token und der erneut eingespielte Refresh-Token aus Schritt 4. Wenn Sie die Authentifizierungsregeln erweitern, erweitern Sie zuerst diese Ablehnungstests; sie sind es, die einen kaputten Guard aufdecken. + +## Rückblick + +- **Generierung**: `fastkit startdemo fastapi-auth-jwt` → `bash scripts/run-server.sh` → Dokumentation unter `/docs`. +- **Tokens**: kurzlebiger Access-Token für Anfragen, rotierender Refresh-Token, der über `jti` erfasst wird, sodass Wiederholungen fehlschlagen und Logout tatsächlich wirkt. +- **Guards**: `get_current_user`, `get_current_active_superuser` und `require_scopes(...)` als einfache Dependencies. +- **Speicherung**: SQLite ohne Aufwand, PostgreSQL durch Setzen von `DATABASE_URL` und Ausführen von `alembic upgrade head`. +- **Vor dem Deployment**: `SECRET_KEY` festlegen und `DATABASE_URL` auf eine echte Datenbank zeigen lassen. + +## Wie es weitergeht + +- [SQLModel-Persistenz](sqlmodel.md) — dasselbe Domänen-Layout mit Fokus auf asynchronem Datenzugriff, Migrationen und generischem CRUD. +- [Domänenorientiertes Projekt](domain-starter.md) — das Layout, auf dem diese Vorlage aufbaut, ohne die Authentifizierungsmechanik. +- [Welchen Starter soll ich wählen?](../user-guide/choosing-a-starter.md) — wie sich diese Vorlage mit den übrigen vergleicht. diff --git a/docs/de/tutorial/llm-agent.md b/docs/de/tutorial/llm-agent.md new file mode 100644 index 0000000..d854120 --- /dev/null +++ b/docs/de/tutorial/llm-agent.md @@ -0,0 +1,198 @@ +# Streaming-LLM-Agent mit `fastapi-llm-agent` + +Bauen Sie ein Chat-Backend, das Tokens streamt, während sie erzeugt werden, und das tatsächlich etwas *tun* kann — Tools aufrufen, deren Ergebnisse lesen und weitermachen. Dieses Tutorial führt Sie durch die Vorlage `fastapi-llm-agent`: das SSE-Ereignisprotokoll, die Tool-Call-Schleife und ihre Iterationsobergrenze, den Konversationsspeicher hinter einer Schnittstelle und eine Testsuite, die nie auf das Netzwerk zugreift. + +## Was Sie lernen werden + +- Ein Projekt mit `fastkit startdemo fastapi-llm-agent` generieren +- Welche SSE-Ereignistypen ein Client verarbeiten muss und was jeder davon bedeutet +- Wie ein Durchlauf der Tool-Call-Schleife Schritt für Schritt funktioniert +- Ein eigenes Tool hinzufügen +- Den prozessinternen Konversationsspeicher durch einen echten ersetzen +- Warum die Tests deterministisch sind und nichts kosten + +## Voraussetzungen + +- Python 3.12+ +- FastAPI-fastkit installiert (`pip install fastapi-fastkit`) +- Einen Anthropic-API-Schlüssel — nur, um mit einem echten Modell zu sprechen. Die generierte Testsuite läuft ohne Schlüssel. +- Vertrautheit mit `async`/`await` und asynchronen Generatoren + +## Schritt 1: Generieren und konfigurieren + +```console +$ fastkit startdemo fastapi-llm-agent +Enter the project name: support-agent +Enter the author name: Developer Kim +Enter the author email: developer@example.com +Enter the project description: Streaming support assistant +Select package manager (pip, uv, pdm, poetry) [uv]: uv +Do you want to proceed with project creation? [y/N]: y +``` + +Tragen Sie Ihren Schlüssel vor dem ersten Start in `.env` ein: + +```bash +ANTHROPIC_API_KEY=sk-ant-... +LLM_MODEL=claude-sonnet-5 +LLM_MAX_TOKENS=4096 +MAX_TOOL_ITERATIONS=5 +``` + +Der Schlüssel wird von pydantic-settings gelesen und dem Client bei der Erzeugung übergeben. Er wird nie in den Quellcode geschrieben und nie in das Docker-Image eingebacken. + +```console +$ cd support-agent +$ bash scripts/run-server.sh +$ bash scripts/chat.sh "What is 12 * 34?" +``` + +!!! tip "Ein Gateway statt der Anthropic-API verwenden" + Setzen Sie `LLM_BASE_URL` (und `LLM_AUTH_TOKEN`, falls das Gateway Bearer-Authentifizierung verwendet), um auf LiteLLM, einen internen Proxy oder einen lokalen Server zu zeigen. Das Übertragungsformat bleibt die Anthropic Messages API; die Übersetzung für andere Anbieter ist Aufgabe des Gateways, nicht der App. + +## Schritt 2: Der generierte Baum + +``` +support-agent/ +├── pyproject.toml # PEP 621 metadata + [tool.fastapi-fastkit] +├── requirements.txt +├── Dockerfile +├── .env +├── scripts/ +│ ├── chat.sh # send one message to a running server +│ ├── format.sh lint.sh run-server.sh test.sh +├── src/ +│ └── app/ +│ ├── main.py # FastAPI app + lifespan +│ ├── core/config.py # pydantic-settings (API key lives here) +│ ├── llm/ +│ │ ├── client.py # AsyncAnthropic factory +│ │ ├── tools.py # tool schemas + handlers +│ │ └── agent.py # streaming agent + tool loop +│ ├── memory/ +│ │ ├── base.py # ConversationStore interface +│ │ └── in_memory.py # process-local implementation +│ ├── schemas/chat.py # request bodies + SSE event envelope +│ └── api/ +│ ├── router.py # aggregates health + chat +│ ├── deps.py # store / agent dependencies +│ ├── health.py # GET /health +│ └── chat.py # POST /chat (SSE), conversation history +└── tests/ + ├── conftest.py # scripted fake Anthropic client + ├── test_health.py test_chat.py test_tools.py test_memory.py +``` + +Drei Grenzen sollten Sie gleich zu Beginn beachten: Der **Client** wird von einer Factory erzeugt (sodass Tests ihn ersetzen können), der **Speicher** ist eine Schnittstelle mit einer trivialen Implementierung (sodass die Produktion sie ersetzen kann), und **Tools** sind eine Schema-Liste plus ein Handler-Dict (sodass das Hinzufügen eines Tools nur eine Datei berührt). + +## Schritt 3: Die Endpunkte + +| Methode | Endpunkt | Beschreibung | +|---------|------------------------------|-------------------------------| +| GET | `/api/v1/health` | Liveness-Probe | +| POST | `/api/v1/chat` | Eine Antwort als SSE streamen | +| GET | `/api/v1/conversations/{id}` | Gespeicherten Verlauf lesen | +| DELETE | `/api/v1/conversations/{id}` | Eine Konversation vergessen | + +`POST /api/v1/chat` nimmt `{"message": "...", "conversation_id": "..."}` entgegen. Lassen Sie `conversation_id` weg, um eine neue Konversation zu beginnen, und lesen Sie die zugewiesene ID aus dem abschließenden `done`-Ereignis aus. + +## Schritt 4: Das Stream-Protokoll + +Jede SSE-Nachricht trägt ihren Typ im Feld `event:` und eine JSON-Hülle in `data:`. Ein Client muss fünf davon plus Fehler verarbeiten: + +| Ereignis | Felder | Bedeutung | +|---------------|---------------------------------|------------------------------------| +| `token` | `text` | Ein Teil der Assistentenantwort | +| `tool_use` | `tool_name`, `tool_input` | Claude hat ein Tool angefordert | +| `tool_result` | `tool_name`, `tool_output` | Das Tool wurde ausgeführt | +| `usage` | `input_tokens`, `output_tokens` | Token-Verbrauch pro Durchlauf | +| `done` | `conversation_id` | Der Durchlauf ist abgeschlossen | +| `error` | `detail` | Mitten im Stream ist etwas fehlgeschlagen | + +`tool_use` und `tool_result` sind keine Dekoration — sie ermöglichen es einer UI, „searching…“ anzuzeigen, statt einer unerklärten Pause, während ein Tool läuft. + +Der Token-Verbrauch wird außerdem bei jedem Durchlauf in den Logger `app.agent` geschrieben, sodass die Kosten in den Server-Logs sichtbar sind, ohne einen Client instrumentieren zu müssen. + +## Schritt 5: Ein Durchlauf der Schleife + +`ChatAgent.stream` in `src/app/llm/agent.py` ist die gesamte Engine. Ein Durchlauf sieht so aus: + +1. Den Konversationsverlauf aus dem Speicher laden. +2. Eine Streaming-Anfrage mit dem Verlauf, der neuen Benutzernachricht und den Tool-Schemas öffnen. +3. Jeden eintreffenden Textabschnitt sofort als `token`-Ereignis ausgeben. +4. Wenn der Stream endet, die finale Nachricht lesen; ein `usage`-Ereignis ausgeben und die Token-Zahlen loggen. +5. **Wenn `stop_reason != "tool_use"`,** ist der Assistent fertig: den Durchlauf speichern, `done` ausgeben, zurückkehren. +6. **Andernfalls** für jeden `tool_use`-Block: `tool_use` ausgeben, den Handler ausführen, `tool_result` ausgeben und die Ausgabe sammeln. +7. Alle Tool-Ergebnisse als eine einzige `user`-Nachricht zurücksenden — das verlangt die Messages API — und ab Schritt 2 wiederholen. + +Die Schleife ist durch `MAX_TOOL_ITERATIONS` begrenzt (standardmäßig 5). Fordert Claude über diese Grenze hinaus weiter Tools an, wird der Durchlauf gespeichert, eine Warnung geloggt und ein `error`-Ereignis ausgegeben, statt den Server endlos weiterlaufen zu lassen: + +```python +yield AgentEvent( + type="error", + detail=( + "The assistant exceeded the tool-call limit of " + f"{self._max_tool_iterations} iterations." + ), +) +``` + +Eine solche Obergrenze ist in einer Agent-Schleife nicht optional. Ohne sie kann Ihnen ein Modell, das ein Tool-Ergebnis missversteht, unbegrenzt Kosten verursachen. + +## Schritt 6: Die mitgelieferten Tools + +| Tool | Zweck | +|----------------|----------------------------------------------------------------------| +| `calculator` | Arithmetik, in einen AST geparst und per Whitelist geprüft — kein `eval` | +| `current_time` | Aktuelle Uhrzeit in einer IANA-Zeitzone | + +Der `calculator` verdient einen zweiten Blick. Er parst den Ausdruck mit `ast` und durchläuft nur Knotentypen aus einer Whitelist, sodass ein von einem Modell stammendes Tool-Argument kein beliebiges Python ausführen kann. Jedes Tool, das modellgenerierte Eingaben akzeptiert, braucht dieselbe Disziplin: **Modellausgabe ist nicht vertrauenswürdige Eingabe.** + +### Ein Tool hinzufügen + +Alles liegt in `src/app/llm/tools.py`: + +1. Hängen Sie ein Schema an `TOOL_SCHEMAS` an — `name`, `description` und ein JSON-Schema als `input_schema`. +2. Registrieren Sie den Handler unter demselben Namen in `TOOL_HANDLERS`. + +Zwei Dinge sollten Sie richtig machen: + +- **Die Beschreibung ist die gesamte Anweisung, die Claude** darüber erhält, wann es zu dem Tool greifen soll. Das ist Prompt-Engineering, keine Dokumentation — vage Beschreibungen führen zu Tools, die nie aufgerufen werden, oder ständig. +- **Handler sollten einen String zurückgeben und ihre eigenen Fehler in eine lesbare Meldung umwandeln,** statt eine Ausnahme auszulösen. Die Schleife gibt diesen Text direkt an das Modell zurück, sodass „no city named X was found“ dem Modell erlaubt, sich zu erholen, während eine Ausnahme den Durchlauf beendet. + +## Schritt 7: Konversationsspeicher + +`ConversationStore` (`src/app/memory/base.py`) ist die Schnittstelle; das mitgelieferte `in_memory.py` implementiert sie mit einem Dict. Das genügt für einen einzelnen Prozess und ist für alles andere falsch — ein Neustart vergisst alles, und zwei Worker sind sich über den Verlauf uneinig. + +Für den echten Einsatz implementieren Sie dieselbe Schnittstelle gegen Redis oder eine Datenbank und geben sie aus `get_store()` in `src/app/api/deps.py` zurück. Am Agent und an den Routern ändert sich nichts, da keiner von beiden jemals die konkrete Klasse nennt. + +## Schritt 8: Die Tests + +```console +$ bash scripts/test.sh # or: pytest +``` + +`tests/conftest.py` spielt vorgeskriptete Durchläufe über ein Fake ab, das `AsyncAnthropic.messages.stream` nachahmt. Daher ist die Suite deterministisch, läuft offline, kostet nichts und deckt dennoch die Pfade ab, auf die es ankommt: den Streaming-Pfad und die vollständige Tool-Schleife, einschließlich eines Durchlaufs, in dem ein Tool aufgerufen wird und das Modell anschließend antwortet. + +Dieses Muster sollten Sie beibehalten, wenn Sie den Agent erweitern. Eine LLM-Integration gegen das Live-Modell zu testen, ergibt eine langsame, unzuverlässige und teure Suite, die trotzdem nicht prüfen kann, was das Modell sagen wird. Die *Schleife* gegen einen vorgeskripteten Client zu testen, prüft genau das, was Sie geschrieben haben. + +## Schritt 9: Erweiterungspunkte + +- **Persistenter Verlauf** — implementieren Sie `ConversationStore` gegen Redis oder eine Datenbank (Schritt 7). +- **RAG** — Retrieval liegt in der Vorlage bewusst außerhalb des Umfangs. Die naheliegende Nahtstelle ist `ChatAgent.stream`: Holen Sie vor der ersten Anfrage Kontext und stellen Sie ihn dem System-Prompt voran, oder stellen Sie Ihren Index als weiteres Tool bereit und lassen Sie das Modell entscheiden, wann es sucht. +- **Authentifizierung und Rate Limiting** — `POST /chat` ist im Auslieferungszustand nicht authentifiziert. Hängen Sie eine Dependency an den Router, bevor Sie ihn öffentlich zugänglich machen; das [Tutorial zur JWT-Authentifizierung](auth-jwt.md) enthält die Guards, die Sie wiederverwenden würden. + +## Rückblick + +- **Generierung**: `fastkit startdemo fastapi-llm-agent`, `ANTHROPIC_API_KEY` setzen, `bash scripts/run-server.sh`. +- **Streaming**: SSE mit den Ereignissen `token` / `tool_use` / `tool_result` / `usage` / `done` / `error`. +- **Schleife**: Stream → Tool-Aufrufe → Ergebnisse als eine Benutzernachricht zurück → wiederholen, begrenzt durch `MAX_TOOL_ITERATIONS`. +- **Tools**: ein Schema in `TOOL_SCHEMAS` und ein Handler in `TOOL_HANDLERS`; behandeln Sie Tool-Eingaben als nicht vertrauenswürdig. +- **Speicher**: `ConversationStore`-Schnittstelle, Dict-Implementierung, Austausch in `get_store()`. +- **Tests**: ein vorgeskripteter Fake-Client, sodass die Suite offline und deterministisch läuft. + +## Wie es weitergeht + +- [MCP-Integration](mcp-integration.md) — wenn Sie einen MCP-Server statt eines Chat-Agents brauchen. +- [JWT-Authentifizierung](auth-jwt.md) — Konten vor den Chat-Endpunkt stellen. +- [Welchen Starter soll ich wählen?](../user-guide/choosing-a-starter.md) diff --git a/docs/de/tutorial/mcp-integration.md b/docs/de/tutorial/mcp-integration.md index 4189256..3afe3b4 100644 --- a/docs/de/tutorial/mcp-integration.md +++ b/docs/de/tutorial/mcp-integration.md @@ -68,8 +68,8 @@ Deploying FastAPI project using 'fastapi-mcp' template │ Dependency 1 │ fastapi │ │ Dependency 2 │ uvicorn │ │ Dependency 3 │ pydantic │ -│ Dependency 4 │ python-jose │ -│ Dependency 5 │ passlib │ +│ Dependency 4 │ PyJWT │ +│ Dependency 5 │ pwdlib[argon2] │ │ Dependency 6 │ python-multipart│ │ Dependency 7 │ mcp │ └──────────────┴────────────────┘ @@ -131,21 +131,21 @@ ai-integrated-api/ ```python from datetime import datetime, timedelta from typing import Optional, Dict, Any -from jose import JWTError, jwt -from passlib.context import CryptContext +import jwt +from pwdlib import PasswordHash from src.core.config import settings -# Password hashing -pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") +# Password hashing — argon2id, the recommended default for new applications. +password_hash = PasswordHash.recommended() def verify_password(plain_password: str, hashed_password: str) -> bool: """Password verification""" - return pwd_context.verify(plain_password, hashed_password) + return password_hash.verify(plain_password, hashed_password) def get_password_hash(password: str) -> str: """Password hashing""" - return pwd_context.hash(password) + return password_hash.hash(password) def create_access_token(data: Dict[str, Any], expires_delta: Optional[timedelta] = None) -> str: """Access token generation""" @@ -189,7 +189,7 @@ def decode_token(token: str) -> Optional[Dict[str, Any]]: algorithms=[settings.ALGORITHM] ) return payload - except JWTError: + except jwt.PyJWTError: return None def verify_token(token: str, token_type: str = "access") -> Optional[str]: @@ -452,8 +452,7 @@ user_db = UserDatabase() from typing import Optional, List from fastapi import Depends, HTTPException, status, Security from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials, APIKeyHeader -from jose import JWTError - +import jwt from src.auth.jwt_handler import decode_token, token_manager from src.auth.models import User, UserInDB, Permission, user_db @@ -486,7 +485,7 @@ async def get_current_user( if user_id is None: raise credentials_exception - except JWTError: + except jwt.PyJWTError: raise credentials_exception user = user_db.get_user_by_id(user_id) diff --git a/docs/de/tutorial/sqlmodel.md b/docs/de/tutorial/sqlmodel.md new file mode 100644 index 0000000..c0a6d5b --- /dev/null +++ b/docs/de/tutorial/sqlmodel.md @@ -0,0 +1,260 @@ +# Asynchrone Persistenz mit `fastapi-sqlmodel` + +Bauen Sie eine datenbankgestützte API, in der jedes Konzept **einmal** definiert wird. SQLModel-Klassen sind gleichzeitig die SQLAlchemy-Tabelle und das Pydantic-Schema, sodass das Hinzufügen einer Spalte nicht bedeutet, vier Dateien zu bearbeiten. Dieses Tutorial führt Sie durch die Vorlage `fastapi-sqlmodel`: die Modellhierarchie, die generische CRUD-Basis, paginierte Antworten, asynchrone Alembic-Migrationen und wie Sie Ihre eigene Domäne hinzufügen. + +## Was Sie lernen werden + +- Ein Projekt mit `fastkit startdemo fastapi-sqlmodel` generieren +- Wie `ItemBase` → `Item` / `ItemCreate` / `ItemUpdate` / `ItemPublic` doppelte Schemas beseitigt +- Was `CRUDBase` Ihnen ohne Aufwand bietet und wo Sie es erweitern +- Die Listenhülle `Page[T]` und wo ihre Grenzen durchgesetzt werden +- Asynchrone Alembic-Migrationen ausführen und von SQLite zu PostgreSQL wechseln + +## Voraussetzungen + +- Python 3.12+ +- FastAPI-fastkit installiert (`pip install fastapi-fastkit`) +- Vertrautheit mit `async`/`await` und grundlegenden SQL-Konzepten + +Wenn Sie stattdessen den synchronen, Compose-basierten PostgreSQL-Stack möchten, lesen Sie [Datenbankintegration](database-integration.md), das `fastapi-psql-orm` verwendet. + +## Schritt 1: Generieren und ausführen + +```console +$ fastkit startdemo fastapi-sqlmodel +Enter the project name: catalog-api +Enter the author name: Developer Kim +Enter the author email: developer@example.com +Enter the project description: Product catalog service +Select package manager (pip, uv, pdm, poetry) [uv]: uv +Do you want to proceed with project creation? [y/N]: y +``` + +```console +$ cd catalog-api +$ bash scripts/migrate.sh # alembic upgrade head +$ bash scripts/run-server.sh # or: uvicorn src.app.main:app --reload +``` + +Die Standard-`DATABASE_URL` ist `sqlite+aiosqlite:///./app.db`, sodass Sie keinen Datenbankserver installieren müssen. Die Dokumentation finden Sie unter . + +## Schritt 2: Der generierte Baum + +``` +catalog-api/ +├── pyproject.toml # PEP 621 metadata + [tool.fastapi-fastkit] +├── requirements.txt +├── alembic.ini +├── Dockerfile +├── .env +├── scripts/ +│ ├── format.sh lint.sh migrate.sh run-server.sh test.sh +├── src/ +│ └── app/ +│ ├── main.py # FastAPI app + lifespan +│ ├── core/config.py # pydantic-settings +│ ├── db/ +│ │ ├── session.py # async engine, get_session dependency +│ │ └── base.py # metadata aggregation for Alembic +│ ├── crud/ +│ │ ├── base.py # generic CRUDBase +│ │ └── pagination.py # Page[T] envelope +│ ├── alembic/ +│ │ ├── env.py # async migration environment +│ │ └── versions/0001_create_items_table.py +│ ├── api/ +│ │ ├── router.py # aggregates health + domain routers +│ │ └── health.py # GET /health +│ └── domains/items/ # the example domain +│ ├── models.py # Item table + Create/Update/Public schemas +│ ├── crud.py # ItemCRUD(CRUDBase) +│ └── router.py # FastAPI router +└── tests/ + ├── conftest.py test_health.py test_items.py test_crud_base.py +``` + +Beachten Sie, was ein Domänenordner *nicht* enthält: keine von `models.py` getrennte `schemas.py` und keinen Repository-Boilerplate pro Domäne. Beides geht in SQLModel bzw. in `CRUDBase` auf. + +## Schritt 3: Eine Modelldefinition pro Konzept + +Öffnen Sie `src/app/domains/items/models.py`. Alles leitet sich von einer einzigen Basis ab: + +```python +class ItemBase(SQLModel): + """Fields shared by the table and every request/response schema.""" + name: str = Field(min_length=1, max_length=128, index=True) + description: str | None = Field(default=None, max_length=512) + price: float = Field(ge=0) + in_stock: bool = True + + +class Item(ItemBase, table=True): # the actual table + __tablename__ = "items" + id: int | None = Field(default=None, primary_key=True) + created_at: datetime = Field(default_factory=_utcnow) + + +class ItemCreate(ItemBase): ... # POST body — no server-assigned fields + + +class ItemUpdate(SQLModel): # PATCH body — every field optional + name: str | None = Field(default=None, min_length=1, max_length=128) + ... + + +class ItemPublic(ItemBase): # response — base + server-assigned + id: int + created_at: datetime +``` + +Vier Klassen, vier klar getrennte Aufgaben, ein Ort, an dem die Felder liegen: + +| Klasse | Rolle | Warum sie getrennt ist | +|--------------|---------------------------------|------------------------| +| `ItemBase` | gemeinsame Felddefinitionen | die einzige Quelle der Wahrheit | +| `Item` | die Datenbanktabelle | fügt `id`, `created_at` hinzu | +| `ItemCreate` | Request-Body für `POST` | darf `id` nicht akzeptieren | +| `ItemUpdate` | Request-Body für `PATCH` | jedes Feld optional | +| `ItemPublic` | Response-Body | gibt vom Server vergebene Felder aus | + +**Der Gewinn:** Eine `sku`-Spalte hinzuzufügen bedeutet, eine Zeile in `ItemBase` zu ergänzen. Die Tabelle erhält die Spalte, der Create-Body akzeptiert sie und die Antwort gibt sie zurück — ohne weitere Änderungen. `ItemUpdate` bleibt absichtlich explizit: Die Semantik partieller Updates ist eine Entscheidung, keine Ableitung. + +## Schritt 4: CRUD, das Sie nicht neu schreiben + +`src/app/crud/base.py` ist eine generische Klasse, die über das Tabellenmodell und seine beiden Eingabeschemas parametrisiert ist: + +```python +class CRUDBase(Generic[ModelType, CreateSchemaType, UpdateSchemaType]): + async def get(self, session, obj_id) -> ModelType | None: ... + async def list(self, session, *, offset=0, limit=20) -> Sequence[ModelType]: ... + async def count(self, session) -> int: ... + async def create(self, session, obj_in) -> ModelType: ... + async def update(self, session, db_obj, obj_in) -> ModelType: ... + async def delete(self, session, db_obj) -> None: ... +``` + +Ein Detail, das Sie verinnerlichen sollten — `update` verwendet `exclude_unset=True`: + +```python +changes: dict[str, Any] = obj_in.model_dump(exclude_unset=True) +``` + +Genau das macht `PATCH` tatsächlich partiell. Ein Feld, das der Client nicht gesendet hat, fehlt im Dict und wird daher nie geschrieben. Ohne `exclude_unset` würde ein `PATCH` unberührte Spalten stillschweigend mit ihren Schema-Standardwerten überschreiben. + +Eine Domäne leitet von der Basis ab und fügt nur hinzu, was für sie spezifisch ist: + +```python +class ItemCRUD(CRUDBase[Item, ItemCreate, ItemUpdate]): + """CRUD for Item, extended with a name search.""" + + async def search_by_name(self, session, term, *, limit=20): + statement = select(Item).where(Item.name.like(f"%{term}%")).limit(limit) + result = await session.exec(statement) + return result.all() + + +item_crud = ItemCRUD(Item) +``` + +## Schritt 5: Paginierte Listen + +Listen-Endpunkte geben statt eines nackten Arrays eine `Page[T]`-Hülle zurück: + +```json +{"items": [...], "total": 42, "offset": 0, "limit": 20} +``` + +Die Begrenzung erfolgt im Router, nicht im Client: + +```python +limit: int = Query(default=settings.DEFAULT_PAGE_LIMIT, ge=1) +... +limit = min(limit, settings.MAX_PAGE_LIMIT) +``` + +Daher kann `?limit=100000` aus einer Anfrage keinen vollständigen Tabellenscan machen. `Page` stellt außerdem eine Property `has_next` bereit, die aus `offset`, `len(items)` und `total` abgeleitet wird und meist genau das ist, was ein Frontend tatsächlich braucht. + +Die Endpunkte, die die items-Domäne mitbringt: + +| Methode | Endpunkt | Beschreibung | +|---------|---------------------------|------------------------------------| +| GET | `/api/v1/health` | Liveness-Probe | +| GET | `/api/v1/items` | Items auflisten (`offset`, `limit`) | +| GET | `/api/v1/items/{item_id}` | Ein einzelnes Item lesen | +| POST | `/api/v1/items` | Ein Item anlegen (`201`) | +| PATCH | `/api/v1/items/{item_id}` | Ein Item teilweise aktualisieren | +| DELETE | `/api/v1/items/{item_id}` | Ein Item löschen (`204`) | + +Probieren Sie es aus: + +```console +$ curl -X POST http://127.0.0.1:8000/api/v1/items \ + -H 'Content-Type: application/json' \ + -d '{"name": "Keyboard", "price": 89.0}' + +$ curl 'http://127.0.0.1:8000/api/v1/items?offset=0&limit=5' +``` + +## Schritt 6: Durchgehend asynchron + +`src/app/db/session.py` erstellt eine `AsyncEngine` pro Prozess und gibt Sessions mit Request-Gültigkeitsbereich aus: + +```python +async def get_session() -> AsyncGenerator[AsyncSession, None]: + async with async_session_factory() as session: + yield session +``` + +Router hängen davon ab (`session: AsyncSession = Depends(get_session)`), `CRUDBase` nimmt sie als erstes Argument entgegen, und jeder Datenbankaufruf wird mit `await` ausgeführt. Nichts im Request-Pfad blockiert die Event-Loop durch I/O. + +`create_db_and_tables()` liegt ebenfalls hier und wird aus dem `lifespan` der App aufgerufen. Es ist eine Erleichterung für den SQLite-Standard und für Tests — sobald Alembic das Schema verwaltet, setzen Sie `CREATE_TABLES_ON_STARTUP=false`. + +## Schritt 7: Eine Domäne hinzufügen + +1. Legen Sie `src/app/domains//` nach dem Vorbild von `items` an (`models.py`, `crud.py`, `router.py`). +2. **Importieren Sie das Tabellenmodell in `src/app/db/base.py`.** Diesen Schritt vergisst man leicht, und er ist verwirrend zu debuggen: Alembic-Autogenerate und `create_db_and_tables` sehen nur Modelle, die importiert wurden, daher zeigt sich ein fehlender Import als leere Migration statt als Fehler. +3. Registrieren Sie den Router in `src/app/api/router.py`. Die Anker `# fastkit:imports` und `# fastkit:routes` markieren, wo `fastkit addroute` Code einfügt — setzen Sie Ihren Code an dieselben Stellen. +4. Generieren Sie eine Migration: `alembic revision --autogenerate -m "add "`. +5. Fügen Sie `tests/test_.py` nach dem Vorbild von `tests/test_items.py` hinzu. + +## Schritt 8: Migrationen und PostgreSQL + +Die Alembic-Umgebung unter `src/app/alembic/env.py` ist asynchron und läuft daher auf demselben Treiber-Stack wie die App. Im Alltag: + +```console +$ bash scripts/migrate.sh # upgrade head +$ alembic revision --autogenerate -m "add sku to items" # after a model change +``` + +Ein Datenbankwechsel besteht aus einer geänderten URL und einem Treiber: + +```console +$ uv pip install "asyncpg>=0.31.0" +$ export DATABASE_URL="postgresql+asyncpg://user:password@localhost:5432/app" +$ bash scripts/migrate.sh +``` + +An Modellen, CRUD oder Routern ändert sich nichts — genau darum geht es, wenn der Treiber hinter `create_engine` bleibt. + +## Schritt 9: Die Tests + +```console +$ bash scripts/test.sh # or: pytest +``` + +Jeder Test läuft gegen eine frische In-Memory-SQLite-Datenbank: keine Migrationen, keine Fixture-Dateien, kein Aufräumen zwischen Tests. `test_crud_base.py` testet die generische Basis direkt — es lohnt sich, die Datei zu lesen, bevor Sie die Basis erweitern, denn sie dokumentiert das `exclude_unset`-Verhalten und den count/offset-Vertrag, den Ihre eigenen Domänen erben. + +## Rückblick + +- **Generierung**: `fastkit startdemo fastapi-sqlmodel` → `bash scripts/migrate.sh` → `bash scripts/run-server.sh`. +- **Modelle**: `ItemBase` enthält die Felder; Tabelle und Ein-/Ausgabeschemas leiten sich davon ab, sodass eine neue Spalte nur einmal deklariert wird. +- **CRUD**: `CRUDBase` liefert get / list / count / create / update / delete; Domänen leiten davon ab, um eigene Abfragen hinzuzufügen. +- **Listen**: `Page[T]`-Hülle mit serverseitiger Begrenzung des Limits. +- **Schema**: asynchrones Alembic; denken Sie daran, neue Modelle in `src/app/db/base.py` zu importieren. +- **Datenbank**: standardmäßig SQLite, PostgreSQL durch Ändern von `DATABASE_URL` und Installieren von `asyncpg`. + +## Wie es weitergeht + +- [JWT-Authentifizierung](auth-jwt.md) — stellen Sie Konten und geschützte Routen vor diese Datenschicht. +- [Domänenorientiertes Projekt](domain-starter.md) — das Layout, das beide Vorlagen teilen, ohne Datenbank. +- [Welchen Starter soll ich wählen?](../user-guide/choosing-a-starter.md) diff --git a/docs/de/user-guide/choosing-a-starter.md b/docs/de/user-guide/choosing-a-starter.md index c89694d..2a003b5 100644 --- a/docs/de/user-guide/choosing-a-starter.md +++ b/docs/de/user-guide/choosing-a-starter.md @@ -17,6 +17,9 @@ Der Rest dieser Seite erklärt, warum das so ist und wann eine andere Wahl sinnv | Möchten das kleinstmögliche Gerüst | `fastkit init --interactive` (Preset: **`minimal`**) | | Schreiben einen schnellen Prototyp / ein Single-File-Skript | `fastkit init --interactive` (Preset: **`single-module`**) | | Brauchen eine echte Datenbank (PostgreSQL + SQLAlchemy + Alembic) | `fastkit startdemo fastapi-psql-orm` | +| Möchten asynchrone Persistenz mit Migrationen und generischem CRUD | `fastkit startdemo fastapi-sqlmodel` | +| Brauchen vom ersten Tag an Benutzerkonten, Login und geschützte Routen | `fastkit startdemo fastapi-auth-jwt` | +| Bauen ein LLM-Chat-Backend mit Streaming und Tools | `fastkit startdemo fastapi-llm-agent` | | Möchten ein produktionsnahes domänenorientiertes Layout für eine mittelgroße API | `fastkit init --interactive` (Preset: **`domain-starter`**) | ## `startdemo` vs. `init --interactive` — wo ist der Unterschied? @@ -112,11 +115,33 @@ Für den vollständigen Vertrag pro Funktion (Zielpfade von Datenbank-/Auth-Konf | `fastapi-empty` | `minimal` | Nacktes Gerüst; gleiche Form, die `minimal` produziert. | | `fastapi-single-module` | `single-module` | Single-File-Demo. | | `fastapi-domain-starter` | `domain-starter` | Empfohlener moderner Standard; bringt ein items-Domänenbeispiel mit. | -| `fastapi-async-crud` | `classic-layered` | Asynchrone Variante von `fastapi-default`. | +| `fastapi-auth-jwt` | `domain-starter` | Ihre API braucht Konten. Access-/Refresh-Tokens, argon2id-Hashing, Rollen und Scopes, Alembic-Migrationen. | +| `fastapi-sqlmodel` | `domain-starter` | Asynchrone Persistenz zuerst: SQLModel + asynchrones SQLAlchemy, Migrationen, eine generische CRUD-Basis und paginierte Listen. | +| `fastapi-llm-agent` | `domain-starter` | Ein streamendes Claude-Chat-Backend mit einer echten Tool-Call-Schleife und Offline-Tests. | | `fastapi-custom-response` | `classic-layered` | Demonstriert benutzerdefinierte Response-Hüllen / Formatierung. | -| `fastapi-dockerized` | `classic-layered` | Fügt dem Standard-Layout ein produktionstaugliches Dockerfile hinzu. | -| `fastapi-psql-orm` | (kein direktes Preset) | PostgreSQL + SQLAlchemy + Alembic. Wählen Sie dies, wenn Sie eine echte Datenbank brauchen. | +| `fastapi-psql-orm` | (kein direktes Preset) | PostgreSQL + SQLAlchemy + Alembic, synchron. Wählen Sie dies, wenn Sie einen Compose-Stack rund um eine echte Datenbank möchten. | | `fastapi-mcp` | (kein direktes Preset) | Integration des Model Context Protocol. | +| `fastapi-async-crud` | `classic-layered` | ⚠️ **Veraltet.** Asynchrone Variante von `fastapi-default`; bevorzugen Sie `fastapi-sqlmodel`. | +| `fastapi-dockerized` | `classic-layered` | ⚠️ **Veraltet.** Fügt dem Standard-Layout ein Dockerfile hinzu; jede aktuelle Vorlage liefert bereits eines mit. | + +### Die drei neueren Starter + +Alle drei wurden in v1.4.0 hinzugefügt und folgen der Form von `domain-starter` (`src/app/` mit Ordnern pro Konzept), sodass sie zu dem passen, was Sie von diesem Preset bereits kennen. + +**`fastapi-auth-jwt` — Authentifizierung, die den Kontakt mit der Produktion übersteht.** Access-Tokens sind kurzlebig; Refresh-Tokens rotieren bei jeder Verwendung und werden über ihren `jti` in der Datenbank erfasst, sodass ein erneut eingespielter Token abgelehnt statt stillschweigend akzeptiert wird. Passwörter werden über `pwdlib` mit argon2id gehasht. Logout wirkt tatsächlich: Sie können eine Sitzung oder alle Sitzungen eines Benutzers widerrufen. Guards werden als Dependencies mitgeliefert — `get_current_user`, `get_current_active_superuser` und eine Factory `require_scopes(...)`. Die Vorlage läuft auf SQLite ohne externe Dienste und wechselt durch Setzen von `DATABASE_URL` zu PostgreSQL. Schritt-für-Schritt-Anleitung: [JWT-Authentifizierung](../tutorial/auth-jwt.md). + +**`fastapi-sqlmodel` — eine Modelldefinition pro Konzept.** Eine SQLModel-Klasse ist zugleich die SQLAlchemy-Tabelle und das Pydantic-Schema, sodass eine neue Spalte nur einmal deklariert wird und jedes Request-/Response-Schema sie erbt. Durchgehend asynchron, asynchrone Alembic-Migrationen, eine `CRUDBase`, die create / read / list / count / update / delete bereitstellt, damit jede Domäne nur schreibt, was für sie spezifisch ist, und Listen-Endpunkte in einer Seitenhülle. Schritt-für-Schritt-Anleitung: [SQLModel-Persistenz](../tutorial/sqlmodel.md). + +**`fastapi-llm-agent` — ein Chat-Backend, kein `POST /completion`-Wrapper.** Tokens werden per SSE an den Client gestreamt, und eine Tool-Call-Schleife läuft, bis das Modell fertig ist oder die Iterationsobergrenze greift. Der Konversationsverlauf liegt hinter einer `ConversationStore`-Schnittstelle, sodass der Wechsel vom mitgelieferten Dict zu Redis nur eine Implementierung erfordert. Die Testsuite ersetzt den Anthropic-Client durch ein vorgeskriptetes Fake — deterministisch, offline, ohne API-Schlüssel. Schritt-für-Schritt-Anleitung: [LLM-Agent](../tutorial/llm-agent.md). + +### Zu den veralteten Vorlagen + +`fastapi-async-crud` und `fastapi-dockerized` werden weiterhin mitgeliefert und erzeugen weiterhin funktionierende Projekte — nichts geht kaputt, wenn Sie sie verwenden. Sie sind als veraltet markiert, weil der Grund, sie zu wählen, weggefallen ist: + +- `fastapi-dockerized` existierte, um ein Dockerfile hinzuzufügen. Jede aktuelle Vorlage liefert eines mit, und die interaktive Deployment-Auswahl erzeugt Docker-Dateien für jedes Preset. +- `fastapi-async-crud` demonstrierte asynchrone Endpunkte über einem Mock-Speicher. `fastapi-sqlmodel` macht dasselbe gegen eine echte asynchrone Datenbank, mit Migrationen. + +Bestehende Projekte, die aus einer der beiden Vorlagen erzeugt wurden, erfordern keine Maßnahmen. `fastkit list-templates` zeigt die aktuelle Liste mit einzeiligen Beschreibungen. @@ -141,5 +166,6 @@ Die [Architektur-Preset-Matrix](../reference/preset-feature-matrix.md) ist die R - [Schnellstart](quick-start.md) — erstellen Sie tatsächlich Ihr erstes Projekt. - [Projekte erstellen](creating-projects.md) — ausführlichere Vorstellung der CLI-Optionen. +- [Tutorial JWT-Authentifizierung](../tutorial/auth-jwt.md), [Tutorial SQLModel-Persistenz](../tutorial/sqlmodel.md) und [Tutorial LLM-Agent](../tutorial/llm-agent.md) — vollständige Anleitungen zu den drei in v1.4.0 hinzugefügten Startern. - [Tutorial Domänenorientiertes Projekt](../tutorial/domain-starter.md) — wenn Sie `domain-starter` gewählt haben, führt Sie dieses Tutorial Schritt für Schritt durch den generierten Projektbaum, das mitgelieferte `items`-Beispiel und das Hinzufügen Ihrer nächsten Domäne. - [Architektur-Preset-Matrix](../reference/preset-feature-matrix.md) — der vollständige Vertrag pro Preset / pro Funktion. diff --git a/docs/de/user-guide/cli-reference.md b/docs/de/user-guide/cli-reference.md index c8d5ed2..0edf75d 100644 --- a/docs/de/user-guide/cli-reference.md +++ b/docs/de/user-guide/cli-reference.md @@ -60,9 +60,127 @@ $ fastkit init [OPTIONS] | Option | Beschreibung | Standard | |--------|-------------|---------| +| `--interactive` | Geführte Einrichtung: zuerst Architektur-Preset, dann Funktionsauswahl | aus | +| `--config ` | Das Projekt ohne Eingabeaufforderungen aus einer gespeicherten Konfigurationsdatei erstellen | - | +| `--save-config ` | Zusammen mit `--interactive` die beantwortete Konfiguration unter diesem Pfad speichern | - | +| `--project-name` | Projektname (wird abgefragt, falls weggelassen) | - | +| `--author` | Name des Autors (wird abgefragt, falls weggelassen) | - | +| `--author-email` | E-Mail des Autors (wird abgefragt, falls weggelassen) | - | +| `--description` | Projektbeschreibung (wird abgefragt, falls weggelassen) | - | | `--package-manager` | Zu verwendender Paketmanager (pip, uv, pdm, poetry) | uv | +| `--dry-run` | Zeigt die Dateien und Pakete an, die erstellt würden, ohne etwas zu schreiben | aus | +| `--no-venv` | Erstellung der virtuellen Umgebung überspringen (impliziert `--no-install`) | aus | +| `--no-install` | Installation der Abhängigkeiten überspringen | aus | +| `--yes` / `-y` | Beim Bereitstellen des Projekts im aktuellen Verzeichnis nicht nachfragen, bevor vorhandene Dateien überschrieben werden | aus | | `--help` | Hilfe zum Befehl anzeigen | - | +#### Konfigurationsdateien (`--config` / `--save-config`) + +Die Antworten einer interaktiven Sitzung sind ein einfaches Mapping, und dieses Mapping lässt sich über eine Datei weitergeben. So wird die Projekterstellung zu etwas, das Sie committen, prüfen und erneut abspielen können. + +```console +# eine interaktive Sitzung aufzeichnen +$ fastkit init --interactive --save-config fastkit.config.json + +# später ganz ohne Eingabeaufforderungen erneut abspielen +$ fastkit init --config fastkit.config.json +``` + +Wenn Sie `--save-config` nicht übergeben, fragt ein interaktiver Lauf, der bis zum Ende kommt, ob die Auswahl gespeichert werden soll und wohin. Diese Abfrage erscheint nur, wenn ein Mensch am Terminal sitzt — Läufe über Pipes oder in CI bleiben still und aktivieren das Speichern stattdessen über das Flag. + +**Unterstützte Formate** werden anhand der Dateiendung gewählt: + +| Endung | Verfügbarkeit | +|---|---| +| `.json` | Immer (Standardbibliothek) | +| `.toml` | Immer (Standardbibliothek) | +| `.yaml` / `.yml` | Nur wenn PyYAML installiert ist — fastkit fügt es nicht als Laufzeitabhängigkeit hinzu | + +Eine geladene Datei muss auf oberster Ebene ein Mapping mit mindestens `project_name`, `author` und `author_email` enthalten. Sie wird anschließend mit denselben Regeln validiert, die auch die interaktiven Abfragen verwenden (Form des Projektnamens, E-Mail-Format, `all_dependencies` muss eine Liste sein), und jedes Problem wird gemeldet, bevor irgendetwas geschrieben wird. Eine Datei, die nur Funktionsauswahlen auflistet — ohne `all_dependencies` —, wird über den interaktiven Builder erweitert, sodass eine handgeschriebene Konfiguration und eine Assistentensitzung zur selben Paketmenge führen. + +Ein minimales Beispiel: + +```json +{ + "project_name": "orders-api", + "author": "Developer Kim", + "author_email": "developer@example.com", + "description": "Domain-oriented orders service", + "architecture_preset": "domain-starter", + "package_manager": "uv" +} +``` + +#### Schema der Konfigurationsdatei + +Jede Konfigurationsdatei — handgeschrieben oder von `--save-config` erzeugt — durchläuft denselben Normalisierer, bevor sie den Generator erreicht (`normalize_project_config` in `backend/project_builder/config_schema.py`). Ein unbekannter Schlüssel, eine unbekannte Auswahl für eine bekannte Achse oder ein Wert mit falschem Typ wird mit dem betroffenen Namen und der Liste der tatsächlich erlaubten Werte abgelehnt, statt stillschweigend ignoriert zu werden. + +| Schlüssel | Form | Hinweise | +|---|---|---| +| `project_name`, `author`, `author_email` | String | Erforderlich | +| `description` | String | Erforderlich | +| `architecture_preset` (Alias `preset`) | `"minimal"` \| `"single-module"` \| `"classic-layered"` \| `"domain-starter"` | Sind beide Schreibweisen vorhanden, müssen sie übereinstimmen | +| `package_manager` | `"pip"` \| `"uv"` \| `"pdm"` \| `"poetry"` | | +| `database` | String oder `{"type": }` | `PostgreSQL`, `MySQL`, `MongoDB`, `Redis`, `SQLite`, `None` | +| `authentication`, `async_tasks`, `testing`, `caching`, `monitoring`, `migrations`, `logging` | String oder `{"type": }` | Achsen mit Einfachauswahl — die Auswahlmöglichkeiten jeder Achse finden Sie oben im [Funktionskatalog](#interactive-builder-feature-catalog-interactive) | +| `utilities`, `tooling` | Liste von Strings | Achsen mit Mehrfachauswahl — Auswahlmöglichkeiten im selben Katalog | +| `deployment` | Liste von Strings | Teilmenge von `Docker`, `docker-compose`; die Wahl von `docker-compose` zieht `Docker` automatisch nach | +| `custom_packages` | Liste von Strings | Zusätzliche Pakete, die unverändert installiert werden | + +Jede Achse akzeptiert entweder die reine Auswahl (`"authentication": "JWT"`) oder die Mapping-Form `{"type": ...}`, die `database` und die projekteigenen `[tool.fastapi-fastkit]`-Metadaten verwenden (`"authentication": {"type": "JWT"}`) — beide werden in dieselbe kanonische Form normalisiert, und `--save-config` schreibt immer die kanonische Form als reinen String zurück. + +Eine Konfiguration, die die meisten Achsen nutzt: + +```json +{ + "project_name": "orders-api", + "author": "Developer Kim", + "author_email": "developer@example.com", + "description": "Domain-oriented orders service", + "architecture_preset": "domain-starter", + "package_manager": "uv", + "database": "PostgreSQL", + "authentication": "JWT", + "async_tasks": "Celery", + "caching": "Redis", + "migrations": "Alembic", + "tooling": ["ruff", "makefile"], + "deployment": ["Docker"] +} +``` + +Dieselbe Konfiguration als TOML: + +```toml +project_name = "orders-api" +author = "Developer Kim" +author_email = "developer@example.com" +description = "Domain-oriented orders service" +architecture_preset = "domain-starter" +package_manager = "uv" +database = "PostgreSQL" +authentication = "JWT" +async_tasks = "Celery" +caching = "Redis" +migrations = "Alembic" +tooling = ["ruff", "makefile"] +deployment = ["Docker"] +``` + +#### Vorschau und Überspringen von Schritten + +`--dry-run` gibt den Baum aus, der erstellt würde, sowie die Pakete, die installiert würden — zusammen mit dem Paketmanager, der sie installieren würde —, und beendet sich dann, ohne die Festplatte zu berühren: + +```console +$ fastkit init --config fastkit.config.json --dry-run +``` + +`--no-install` stoppt, nachdem die virtuelle Umgebung erstellt wurde; `--no-venv` überspringt auch die Umgebung (und damit die Installation). Beides ist nützlich in Containern, in CI und überall dort, wo die Umgebung außerhalb von fastkit verwaltet wird. + +#### Bereitstellung im aktuellen Verzeichnis und Bestätigung des Überschreibens (`--yes` / `-y`) { #in-place-deployment-and-overwrite-confirmation-yes-y } + +Wenn ein Projekt an Ort und Stelle bereitgestellt wird — in den aktuellen Workspace statt in einen neuen Projektordner —, fragt fastkit „Overwrite these files?“, bevor eine bereits vorhandene Datei ersetzt wird. `--yes` (oder die Kurzform `-y`) überspringt diese Bestätigung und überschreibt direkt. Die separate Abfrage „Do you want to proceed with project creation?“ ist davon nicht betroffen und wird weiterhin angezeigt. In einer nicht-interaktiven Umgebung (stdin ist kein TTY, etwa in CI oder bei einem Lauf über eine Pipe) wird die Bestätigung des Überschreibens auch ohne `--yes` automatisch übersprungen. + #### Interaktive Eingabeaufforderungen Der Befehl `init` fragt Sie ab: @@ -96,6 +214,27 @@ Der Befehl `init` fragt Sie ab: - `redis` — In-Memory-Datenspeicher - `celery` — verteilte Aufgabenwarteschlange +#### Funktionskatalog des interaktiven Builders (`--interactive`) { #interactive-builder-feature-catalog-interactive } + +`fastkit init --interactive` durchläuft statt des obigen Stacks minimal/standard/full einen anderen, achsenbasierten Katalog (maßgebliche Quelle: `FastkitConfig.PACKAGE_CATALOG` in `core/settings.py` und `DynamicConfigGenerator.build_artifacts()` für das, was jede Auswahl erzeugt). Jede Achse hat den Standardwert `None` (überspringen); `utilities` und `tooling` erlauben eine Mehrfachauswahl. + +Jede Auswahl erzeugt inzwischen echten Code, nicht nur installierte Pakete — wie sich `minimal` / `single-module` (regenerierte `main.py`) und `classic-layered` / `domain-starter` (erhaltene `main.py`, Warnungen zur manuellen Verdrahtung) unterscheiden, zeigt die [Architektur-Preset- / Funktionsmatrix](../reference/preset-feature-matrix.md). + +| Achse | Auswahlmöglichkeiten | Installierte Pakete | Generierte Dateien | +|---|---|---|---| +| `database` | PostgreSQL, MySQL, MongoDB, Redis, SQLite, None | PostgreSQL: `asyncpg`, `sqlalchemy` · MySQL: `aiomysql`, `sqlalchemy` · MongoDB: `motor` · Redis: `redis[hiredis]` · SQLite: `sqlalchemy`, `aiosqlite` | Ein Datenbank-Konfigurationsmodul am Pfad des Presets (z. B. `src/config/database.py`) | +| `authentication` | JWT, OAuth2, FastAPI-Users, Session-based, None | JWT: `pyjwt[crypto]`, `pwdlib[argon2]` · OAuth2: `authlib`, `itsdangerous`, `httpx` · FastAPI-Users: `fastapi-users[sqlalchemy]`, `pyjwt[crypto]`, `pwdlib[argon2]` · Session-based: `itsdangerous` | Ein Auth-Konfigurationsmodul am Pfad des Presets; OAuth2 und Session-based ergänzen zusätzlich das Middleware-Setup in `main.py` | +| `async_tasks` | Celery, Dramatiq, None | Celery: `celery[redis]`, `redis[hiredis]` · Dramatiq: `dramatiq[redis]`, `redis[hiredis]` | `/worker.py` (Hintergrund-Worker) + `/features/tasks.py` (Task-Routen) | +| `testing` | Basic, Coverage, Advanced, None | Basic: `pytest`, `pytest-asyncio`, `httpx` · Coverage: + `pytest-cov` · Advanced: + `faker`, `factory-boy` | `pytest.ini`; Advanced fügt zusätzlich `tests/factories.py` und `tests/test_factories.py` hinzu | +| `caching` | Redis, None | Redis: `redis[hiredis]`, `fastapi-cache2`, `jinja2` | `/features/cache.py` (gecachte Endpunkte) | +| `monitoring` | Loguru, OpenTelemetry, Prometheus, None | Loguru: `loguru` · OpenTelemetry: `opentelemetry-api`, `opentelemetry-sdk`, `opentelemetry-instrumentation-fastapi`, `opentelemetry-exporter-otlp-proto-http` · Prometheus: `prometheus-client`, `prometheus-fastapi-instrumentator` | Keine eigene Datei — direkt in `main.py` verdrahtet (Import / Lifespan / Setup) | +| `utilities` (Mehrfachauswahl) | CORS, Rate-Limiting, Pagination, WebSocket, None | CORS: keine (in FastAPI integriert) · Rate-Limiting: `slowapi` · Pagination: `fastapi-pagination` · WebSocket: `websockets` | CORS und Rate-Limiting werden direkt in `main.py` verdrahtet; Pagination fügt `/features/pagination.py` hinzu, WebSocket fügt `/features/websocket.py` hinzu | +| `migrations` | Alembic, None | Alembic: `alembic` | `alembic.ini`, `alembic/env.py`, `alembic/script.py.mako`, eine Basis-Revision `alembic/versions/_initial.py`, `scripts/migrate.sh` — nur wenn die Datenbankachse eine SQL-Datenbank ist | +| `tooling` (Mehrfachauswahl) | ruff, pre-commit, github-actions, devcontainer, makefile, None | ruff: `ruff` · pre-commit: `pre-commit` · github-actions / devcontainer / makefile: keine | ruff fügt einen `[tool.ruff]`-Block in `pyproject.toml` ein (oder schreibt `ruff.toml` bei `pip`-Projekten); pre-commit schreibt `.pre-commit-config.yaml`; github-actions schreibt `.github/workflows/test.yml`; devcontainer schreibt `.devcontainer/devcontainer.json`; makefile schreibt `Makefile` | +| `logging` | structured, None | Keine (nur stdlib `logging` + `json`) | `/logging_config.py` (strukturiertes JSON-Logging + Request-ID-Middleware), in `main.py` verdrahtet | + +Unabhängig von der Auswahl erhält jedes generierte Projekt die Endpunkte `/health` und `/ready` in `main.py`, und `--dry-run` gibt genau diese Dateimenge aus, bevor irgendetwas geschrieben wird. + #### Beispiele
@@ -227,6 +366,26 @@ Erstellt vollständige CRUD-Endpunkte: | `PUT` | `/api/v1/users/{user_id}` | Nutzer aktualisieren | | `DELETE` | `/api/v1/users/{user_id}` | Nutzer löschen | +#### Wo der Code eingefügt wird + +`addroute` braucht zwei Dinge vom Projekt: wohin die neuen Imports kommen und wo der Router registriert wird. Vorlagen und generierte Einstiegspunkte markieren beides mit Anker-Kommentaren: + +```python +# src/app/api/router.py +from src.app.api import health + +# fastkit:imports + +api_router = APIRouter() +api_router.include_router(health.router) + +# fastkit:routes +``` + +Lassen Sie diese Kommentare beim Bearbeiten der Datei an ihrem Platz — `addroute` fügt direkt oberhalb von `# fastkit:imports` und `# fastkit:routes` ein. Ein Projekt, dem sie fehlen (oder das erzeugt wurde, bevor es die Anker gab), funktioniert trotzdem: fastkit greift auf ein AST-basiertes Einfügen zurück, das den Importblock und die Router-Registrierungen selbst findet. Die Anker machen das Ergebnis lediglich vorhersehbar. + +`addroute` liest außerdem den `[tool.fastapi-fastkit]`-Block des Projekts, um den Einstiegspunkt zu finden, und funktioniert daher auch bei Layouts, die ein reiner Scan des Dateisystems falsch einordnen würde. + ### `startdemo` Ein FastAPI-Projekt aus einer vorgefertigten Vorlage erstellen. @@ -241,9 +400,19 @@ $ fastkit startdemo [OPTIONS] | Option | Beschreibung | Standard | |--------|-------------|---------| +| `--project-name` | Projektname (wird abgefragt, falls weggelassen) | - | +| `--author` | Name des Autors (wird abgefragt, falls weggelassen) | - | +| `--author-email` | E-Mail des Autors (wird abgefragt, falls weggelassen) | - | +| `--description` | Projektbeschreibung (wird abgefragt, falls weggelassen) | - | | `--package-manager` | Zu verwendender Paketmanager (pip, uv, pdm, poetry) | uv | +| `--dry-run` | Zeigt die Dateien und Pakete an, die erstellt würden, ohne etwas zu schreiben | aus | +| `--no-venv` | Erstellung der virtuellen Umgebung überspringen (impliziert `--no-install`) | aus | +| `--no-install` | Installation der Abhängigkeiten überspringen | aus | +| `--yes` / `-y` | Beim Bereitstellen des Projekts im aktuellen Verzeichnis nicht nachfragen, bevor vorhandene Dateien überschrieben werden | aus | | `--help` | Hilfe zum Befehl anzeigen | - | +Wie `init` verweigert auch `startdemo` die Ausführung, wenn das Zielprojektverzeichnis bereits existiert (`Error: Project '{name}' already exists.`). `--dry-run` ist die Ausnahme: Da es nie auf die Festplatte schreibt, darf es auch einen Namen verwenden, für den bereits ein Verzeichnis existiert. Was `--yes` beim Bereitstellen in den aktuellen Workspace bewirkt, erfahren Sie oben unter [Bereitstellung im aktuellen Verzeichnis und Bestätigung des Überschreibens](#in-place-deployment-and-overwrite-confirmation-yes-y). + #### Interaktive Eingabeaufforderungen Der Befehl `startdemo` fragt Sie ab: @@ -259,11 +428,19 @@ Der Befehl `startdemo` fragt Sie ab: | Vorlage | Beschreibung | Funktionen | |----------|-------------|----------| | `fastapi-default` | Einfaches FastAPI-Projekt | Basis-CRUD, Mock-Daten | -| `fastapi-async-crud` | Asynchrone Item-Management-API | Async/await, Performance | +| `fastapi-domain-starter` | Domänenorientierter Starter | Ein Ordner pro Geschäftskonzept, `/health` | +| `fastapi-auth-jwt` | JWT-Authentifizierung | Access-/Refresh-Rotation, argon2id, Rollen und Scopes, Alembic | +| `fastapi-sqlmodel` | Asynchrone SQLModel-Persistenz | SQLModel + asynchrones SQLAlchemy, Alembic, generisches CRUD, Paginierung | +| `fastapi-llm-agent` | Streaming-Claude-Agent | SSE-Streaming, Tool-Call-Schleife, Konversationsspeicher | | `fastapi-custom-response` | Benutzerdefiniertes Antwortsystem | Benutzerdefinierte Antworten, Paginierung | -| `fastapi-dockerized` | Dockerisierte FastAPI-API | Docker, produktionsreif | | `fastapi-psql-orm` | PostgreSQL-FastAPI-API | PostgreSQL, SQLAlchemy, Alembic | +| `fastapi-mcp` | Model-Context-Protocol-Server | MCP-Integration | +| `fastapi-single-module` | Sandbox in einer Datei | Ein Modul, keine Paketgrenzen | | `fastapi-empty` | Minimales FastAPI-Projekt | Absolutes Minimum | +| `fastapi-async-crud` | *(veraltet)* Asynchrone Item-Management-API | Abgelöst durch `fastapi-sqlmodel` | +| `fastapi-dockerized` | *(veraltet)* Dockerisierte FastAPI-API | Docker-Tooling ist jetzt in den neueren Vorlagen enthalten | + +Die veralteten Vorlagen erzeugen weiterhin funktionierende Projekte; sie werden lediglich nicht mehr als Ausgangspunkt empfohlen. Siehe [Welchen Starter soll ich wählen?](choosing-a-starter.md). #### Beispiele @@ -331,9 +508,11 @@ INFO: Uvicorn running on http://127.0.0.1:8000 #### Voraussetzungen - Muss aus dem Verzeichnis eines FastAPI-Projekts ausgeführt werden -- Das Projekt muss `src/main.py` mit einer FastAPI-App enthalten +- Das Projekt muss eine FastAPI-App bereitstellen — ermittelt aus `[tool.fastapi-fastkit].app_module`, falls vorhanden, andernfalls durch Suche nach `main.py` - Die virtuelle Umgebung sollte aktiviert sein +Findet weder der gespeicherte Einstiegspunkt noch die Suche ein Modul, meldet `runserver`, dass es `main.py` nicht finden konnte, und bricht ab. + ### `list-templates` Alle verfügbaren FastAPI-Projektvorlagen auflisten. @@ -372,6 +551,35 @@ $ fastkit list-templates
+## Projektmetadaten (`[tool.fastapi-fastkit]`) + +Jedes von fastkit generierte Projekt hält in seiner eigenen `pyproject.toml` fest, wie es erzeugt wurde: + +```toml +[tool.fastapi-fastkit] +managed = true +version = "1.4.0" +template = "fastapi-domain-starter" +preset = "domain-starter" +package_manager = "uv" +app_module = "src.app.main:app" +features = ["database:PostgreSQL", "authentication:JWT"] +``` + +| Schlüssel | Bedeutung | +|---|---| +| `managed` | Immer `true`; kennzeichnet das Projekt als von fastkit verwaltet | +| `version` | Die fastkit-Version, die das Projekt generiert hat | +| `template` | Die `startdemo`-Vorlage oder die Basisvorlage des Presets | +| `preset` | Das Architektur-Preset von `init --interactive` (sonst weggelassen) | +| `package_manager` | Der Paketmanager, mit dem die Umgebung aufgebaut wurde | +| `app_module` | Der uvicorn-Einstiegspunkt im Format `module:attr` | +| `features` | Flache Liste `":"` der interaktiven Auswahlen | + +Dieser Block ist die einzige maßgebliche Quelle für den Einstiegspunkt des Projekts: `fastkit runserver` liest daraus `app_module`, bevor es auf das Durchsuchen des Baums zurückgreift, `fastkit addroute` nutzt ihn, um die Anwendung zu finden, und generierte Dockerfiles verwenden denselben Wert in ihrem `CMD`. + +Der Block wird ganz am Ende der Generierung geschrieben — Paketmanager schreiben `pyproject.toml` beim Auflösen der Abhängigkeiten neu, daher muss er danach eingetragen werden —, und eine spätere Regenerierung ersetzt ihn, statt eine zweite Kopie anzuhängen. `app_module` von Hand zu bearbeiten, ist der unterstützte Weg, fastkit über ein Layout zu informieren, das Sie selbst umorganisiert haben. + ## Umgebungsvariablen FastAPI-fastkit berücksichtigt folgende Umgebungsvariablen: @@ -381,6 +589,14 @@ FastAPI-fastkit berücksichtigt folgende Umgebungsvariablen: | `FASTKIT_CONFIG_DIR` | Konfigurationsverzeichnis | `~/.fastkit` | | `FASTKIT_TEMPLATES_DIR` | Verzeichnis für benutzerdefinierte Vorlagen | Integrierte Vorlagen | | `FASTKIT_LOG_LEVEL` | Log-Level | `INFO` | +| `FASTKIT_SUBPROCESS_TIMEOUT` | Überschreibt jedes Timeout für Paketmanager-Subprozesse, in Sekunden | je Vorgang (30 / 120 / 900) | + +Aufrufe von Paketmanagern sind einzeln begrenzt, sodass ein hängender Kindprozess das CLI niemals blockieren kann: 30 Sekunden für eine Verfügbarkeitsprüfung, 120 für das Erstellen einer virtuellen Umgebung, 900 für die Installation der Abhängigkeiten. `FASTKIT_SUBPROCESS_TIMEOUT` ersetzt alle drei durch einen einzigen Wert — nützlich in einem langsamen Netzwerk oder um in CI schnell fehlzuschlagen. Ein Wert, der keine positive ganze Zahl ist, wird ignoriert. + +```console +$ export FASTKIT_SUBPROCESS_TIMEOUT=1800 +$ fastkit startdemo fastapi-sqlmodel +``` ### Beispiele diff --git a/docs/de/user-guide/creating-projects.md b/docs/de/user-guide/creating-projects.md index 16ee6af..2ea2aeb 100644 --- a/docs/de/user-guide/creating-projects.md +++ b/docs/de/user-guide/creating-projects.md @@ -462,7 +462,7 @@ Nach der Projekterstellung können Sie weitere Abhängigkeiten hinzufügen:
```console -$ pip install requests httpx python-jose +$ pip install requests httpx pyjwt $ pip freeze > requirements.txt ``` diff --git a/docs/en/contributing/development-setup.md b/docs/en/contributing/development-setup.md index c3d74f5..17b98d3 100644 --- a/docs/en/contributing/development-setup.md +++ b/docs/en/contributing/development-setup.md @@ -403,7 +403,7 @@ FastAPI-fastkit/ - **`scaffolder.py`** - The generation pipeline shared by `init` and `startdemo` - **`transducer.py`** - Template transducer - **`core/`** - Core configuration and exceptions - - **`fragments/`** - Jinja2 fragments rendered into generated projects (see below) + - **`fragments/`** - Jinja2 fragments rendered into generated projects (see above) - **`fastapi_project_template/`** - Project templates (fastapi-default, fastapi-domain-starter, etc.) - **`utils/`** - Shared utility functions - **`tests/`** - Test suite diff --git a/docs/en/reference/translation-status.md b/docs/en/reference/translation-status.md index a4115a9..555d488 100644 --- a/docs/en/reference/translation-status.md +++ b/docs/en/reference/translation-status.md @@ -34,19 +34,18 @@ next section explains). | Locale | Status | Markdown pages | Notes | |---|---|---:|---| | 🇬🇧 English (`en`) | ✅ Source of truth | 29 / 29 | Authoritative. | -| 🇰🇷 Korean (`ko`) | 🟡 Partial | 26 / 29 | Phases 1-3 complete. The three tutorials added in v1.4.0 (`tutorial/auth-jwt.md`, `tutorial/sqlmodel.md`, `tutorial/llm-agent.md`) are not translated yet and fall back to English. `docs/ko/changelog.md` intentionally reuses the canonical English `CHANGELOG.md`. | -| 🇯🇵 Japanese (`ja`) | 🟡 Partial | 26 / 29 | Phases 1-3 complete. The three tutorials added in v1.4.0 (`tutorial/auth-jwt.md`, `tutorial/sqlmodel.md`, `tutorial/llm-agent.md`) are not translated yet and fall back to English. `docs/ja/changelog.md` intentionally reuses the canonical English `CHANGELOG.md`. | -| 🇨🇳 Chinese (`zh`) | 🟡 Partial | 26 / 29 | Phases 1-3 complete. The three tutorials added in v1.4.0 (`tutorial/auth-jwt.md`, `tutorial/sqlmodel.md`, `tutorial/llm-agent.md`) are not translated yet and fall back to English. `docs/zh/changelog.md` intentionally reuses the canonical English `CHANGELOG.md`. | -| 🇪🇸 Spanish (`es`) | 🟡 Partial | 26 / 29 | Phases 1-3 complete. The three tutorials added in v1.4.0 (`tutorial/auth-jwt.md`, `tutorial/sqlmodel.md`, `tutorial/llm-agent.md`) are not translated yet and fall back to English. `docs/es/changelog.md` intentionally reuses the canonical English `CHANGELOG.md`. | -| 🇫🇷 French (`fr`) | 🟡 Partial | 26 / 29 | Phases 1-3 complete. The three tutorials added in v1.4.0 (`tutorial/auth-jwt.md`, `tutorial/sqlmodel.md`, `tutorial/llm-agent.md`) are not translated yet and fall back to English. `docs/fr/changelog.md` intentionally reuses the canonical English `CHANGELOG.md`. | -| 🇩🇪 German (`de`) | 🟡 Partial | 26 / 29 | Phases 1-3 complete. The three tutorials added in v1.4.0 (`tutorial/auth-jwt.md`, `tutorial/sqlmodel.md`, `tutorial/llm-agent.md`) are not translated yet and fall back to English. `docs/de/changelog.md` intentionally reuses the canonical English `CHANGELOG.md`. | - -*Snapshot verified 2026-09-04, for the v1.4.0 release. English gained three -tutorial pages (`tutorial/auth-jwt.md`, `tutorial/sqlmodel.md`, -`tutorial/llm-agent.md`), so every other locale dropped from ✅ Complete to -🟡 Partial at 26 / 29. The Korean landing page, CLI reference and starter -guide were re-synced with their English sources in the same release; the -remaining Korean pages are present but predate the v1.4.0 English updates.* These counts are maintained by hand; +| 🇰🇷 Korean (`ko`) | ✅ Complete | 29 / 29 | Phases 1-3 complete. Re-synced with the v1.4.1 English sources on 2026-10-11: the three v1.4.0 tutorials (`tutorial/auth-jwt.md`, `tutorial/sqlmodel.md`, `tutorial/llm-agent.md`) are translated and pages that had drifted since their original translation were brought back in line. `docs/ko/changelog.md` intentionally reuses the canonical English `CHANGELOG.md`. | +| 🇯🇵 Japanese (`ja`) | ✅ Complete | 29 / 29 | Phases 1-3 complete. Re-synced with the v1.4.1 English sources on 2026-10-11: the three v1.4.0 tutorials (`tutorial/auth-jwt.md`, `tutorial/sqlmodel.md`, `tutorial/llm-agent.md`) are translated and pages that had drifted since their original translation were brought back in line. `docs/ja/changelog.md` intentionally reuses the canonical English `CHANGELOG.md`. | +| 🇨🇳 Chinese (`zh`) | ✅ Complete | 29 / 29 | Phases 1-3 complete. Re-synced with the v1.4.1 English sources on 2026-10-11: the three v1.4.0 tutorials (`tutorial/auth-jwt.md`, `tutorial/sqlmodel.md`, `tutorial/llm-agent.md`) are translated and pages that had drifted since their original translation were brought back in line. `docs/zh/changelog.md` intentionally reuses the canonical English `CHANGELOG.md`. | +| 🇪🇸 Spanish (`es`) | ✅ Complete | 29 / 29 | Phases 1-3 complete. Re-synced with the v1.4.1 English sources on 2026-10-11: the three v1.4.0 tutorials (`tutorial/auth-jwt.md`, `tutorial/sqlmodel.md`, `tutorial/llm-agent.md`) are translated and pages that had drifted since their original translation were brought back in line. `docs/es/changelog.md` intentionally reuses the canonical English `CHANGELOG.md`. | +| 🇫🇷 French (`fr`) | ✅ Complete | 29 / 29 | Phases 1-3 complete. Re-synced with the v1.4.1 English sources on 2026-10-11: the three v1.4.0 tutorials (`tutorial/auth-jwt.md`, `tutorial/sqlmodel.md`, `tutorial/llm-agent.md`) are translated and pages that had drifted since their original translation were brought back in line. `docs/fr/changelog.md` intentionally reuses the canonical English `CHANGELOG.md`. | +| 🇩🇪 German (`de`) | ✅ Complete | 29 / 29 | Phases 1-3 complete. Re-synced with the v1.4.1 English sources on 2026-10-11: the three v1.4.0 tutorials (`tutorial/auth-jwt.md`, `tutorial/sqlmodel.md`, `tutorial/llm-agent.md`) are translated and pages that had drifted since their original translation were brought back in line. `docs/de/changelog.md` intentionally reuses the canonical English `CHANGELOG.md`. | + +*Snapshot verified 2026-10-11, after the v1.4.1 release. All six translated +locales were brought to parity with the English sources: the three tutorials +added in v1.4.0 were translated and every page that had drifted since its +original translation was re-synced. Translations can still lag within a +release — if a translated page disagrees with English, trust English.* These counts are maintained by hand; to recount the current state from the repo root, run: ```console @@ -61,6 +60,8 @@ update it (or open a PR / issue noting the drift). Legend: - ✅ **Source of truth** — the locale we author against. +- ✅ **Complete** — every English page has a translated counterpart. + Individual pages may still lag behind an English update within a release. - 🟡 **Partial** — some pages translated; missing pages fall back. - 🔴 **Skeleton** — language switcher entry exists, but no translated pages are checked in yet. The site renders English content under the diff --git a/docs/en/tutorial/mcp-integration.md b/docs/en/tutorial/mcp-integration.md index 3b4b9c9..c1a0f35 100644 --- a/docs/en/tutorial/mcp-integration.md +++ b/docs/en/tutorial/mcp-integration.md @@ -452,6 +452,7 @@ user_db = UserDatabase() from typing import Optional, List from fastapi import Depends, HTTPException, status, Security from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials, APIKeyHeader +import jwt from src.auth.jwt_handler import decode_token, token_manager from src.auth.models import User, UserInDB, Permission, user_db diff --git a/docs/es/contributing/development-setup.md b/docs/es/contributing/development-setup.md index ec8e8c6..212b44d 100644 --- a/docs/es/contributing/development-setup.md +++ b/docs/es/contributing/development-setup.md @@ -212,6 +212,50 @@ El Makefile del proyecto incluye comandos útiles para las tareas de desarrollo | `make inspect-templates-verbose` | Inspecciona con salida detallada | | `make inspect-template` | Inspecciona plantillas concretas (parámetro TEMPLATES) | +El script subyacente acepta tres flags que conviene conocer mientras iteras: +`--offline` (omite las consultas de actualidad en PyPI), `--no-smoke` (omite +el arranque del proyecto generado con uvicorn — el paso más lento) y `--mypy` +(comprueba los tipos del proyecto generado; es opcional porque es lento). +Consulta [Aseguramiento de calidad de las plantillas](../reference/template-quality-assurance.md) +para ver qué hace realmente cada comprobación. + +```console +$ python scripts/inspect-templates.py --templates fastapi-sqlmodel --offline --no-smoke +``` + +### El código generado vive en fragmentos Jinja2 + +Todo lo que `fastkit init --interactive` escribe en un proyecto — `main.py`, +los módulos de base de datos y autenticación, los archivos de Docker, la +configuración de pytest — se renderiza a partir de plantillas en +`src/fastapi_fastkit/fragments/`, no se ensambla a partir de cadenas en +Python. `main.py.j2` no fija ninguna funcionalidad en el código: itera sobre +las listas `import_fragments`, `lifespan_fragments` y `setup_fragments` que +proporciona `DynamicConfigGenerator` e incluye cada una, de modo que las +funcionalidades se combinan libremente. + +Añadir una funcionalidad consiste, a grandes rasgos, en: + +1. Colocar las piezas en `main/imports/`, `main/lifespan/` y/o + `main/setup/` (los fragmentos de lifespan van indentados con cuatro + espacios; los otros dos no). +2. Registrarlas en `DynamicConfigGenerator._main_fragments()`, protegidas + por la consulta de configuración correspondiente. +3. Para un módulo independiente, añadir `/.py.j2` y + renderizarlo con `self._render(...)`. +4. Ampliar la matriz de combinaciones en + `tests/test_backends/test_project_builder_config_generator.py` — esta + comprueba que cada archivo Python renderizado se analiza con `ast.parse`. + +Dos restricciones con las que es fácil tropezar: cada fragmento debe terminar +con un salto de línea final (el entorno se construye con +`keep_trailing_newline=True`), y `main.py.j2` debe conservar sus anclas +`# fastkit:imports` / `# fastkit:routes` — `fastkit addroute` inserta código +en ellas. + +El procedimiento de referencia vive junto a los propios fragmentos, en +[`src/fastapi_fastkit/fragments/README.md`](https://github.com/bnbong/FastAPI-fastkit/blob/main/src/fastapi_fastkit/fragments/README.md). + ### Comandos de documentación | Comando | Descripción | @@ -353,13 +397,16 @@ FastAPI-fastkit/ - **`src/fastapi_fastkit/`** — código fuente del paquete principal - **`cli.py`** — punto de entrada principal de la CLI - **`backend/`** — lógica central del backend - - **`inspector.py`** — inspector de plantillas + - **`inspection/`** — pipeline de inspección de plantillas (context, checks, consistency, freshness, smoke, lint, report) + - **`inspector.py`** — fachada sobre `inspection/`, mantenida por la ruta de importación histórica - **`interactive/`** — componentes del modo interactivo (prompts, selectores, validadores) - **`package_managers/`** — implementaciones de los gestores de paquetes (pip, uv, pdm, poetry) - - **`project_builder/`** — utilidades de construcción de proyectos + - **`project_builder/`** — utilidades de construcción de proyectos, incluido el generador de configuración basado en fragmentos + - **`scaffolder.py`** — el pipeline de generación que comparten `init` y `startdemo` - **`transducer.py`** — transducer de plantillas - **`core/`** — configuración central y excepciones - - **`fastapi_project_template/`** — plantillas de proyecto (fastapi-default, fastapi-async-crud, etc.) + - **`fragments/`** — fragmentos Jinja2 que se renderizan en los proyectos generados (ver más arriba) + - **`fastapi_project_template/`** — plantillas de proyecto (fastapi-default, fastapi-domain-starter, etc.) - **`utils/`** — funciones de utilidad compartidas - **`tests/`** — suite de pruebas - **`test_backends/`** — pruebas específicas del backend diff --git a/docs/es/contributing/template-creation-guide.md b/docs/es/contributing/template-creation-guide.md index 57cce7c..54378de 100644 --- a/docs/es/contributing/template-creation-guide.md +++ b/docs/es/contributing/template-creation-guide.md @@ -97,8 +97,7 @@ fastapi-{template-name}/ │ ├── lint.sh-tpl # Linting │ ├── run-server.sh-tpl # Ejecución del servidor │ └── test.sh-tpl # Ejecución de pruebas -├── pyproject.toml-tpl # ✅ Metadatos primarios (PEP 621, preferido) -├── setup.py-tpl # 🟡 Metadatos legacy (aceptados por compatibilidad) +├── pyproject.toml-tpl # ✅ Metadatos primarios (PEP 621, obligatorio) ├── requirements.txt-tpl # 🟡 Opcional si pyproject declara dependencias ├── setup.cfg-tpl # Configuración de herramientas de desarrollo ├── README.md-tpl # ✅ Documentación del proyecto (requerida) @@ -110,10 +109,16 @@ fastapi-{template-name}/ - Un directorio `tests/` - `README.md-tpl` -- Al menos un archivo de metadatos: `pyproject.toml-tpl` (preferido, PEP 621) o `setup.py-tpl` (legacy, todavía aceptado) -- Una declaración de `fastapi` como dependencia en al menos uno de: `pyproject.toml-tpl` `[project].dependencies`, `requirements.txt-tpl`, o `setup.py-tpl` `install_requires` +- `pyproject.toml-tpl` (PEP 621) +- Una declaración de `fastapi` como dependencia en `pyproject.toml-tpl` `[project].dependencies` o en `requirements.txt-tpl` -`requirements.txt-tpl` ya no es estrictamente necesario cuando `pyproject.toml-tpl` declara `[project].dependencies`. Las plantillas modernas DEBERÍAN adoptar `pyproject.toml-tpl` como su archivo de metadatos principal. +`requirements.txt-tpl` no es necesario cuando `pyproject.toml-tpl` declara `[project].dependencies` — pero, si una plantilla incluye ambos, deben coincidir: la inspección falla si hay divergencia de dependencias entre ellos. + +**Las plantillas nuevas priorizan pyproject y no deben incluir `setup.py-tpl`.** Desde v1.4.0 ninguna plantilla incluida lo trae. La inspección todavía acepta el archivo para que las plantillas externas sigan funcionando, pero es una vía heredada. + +**Apunta a Python 3.12 de forma coherente**: `requires-python = ">=3.12"`, black `target-version = ["py312"]`, mypy `python_version = "3.12"` y `python:3.12-slim` para cualquier imagen base de Docker. La inspección comprueba que las tres versiones fijadas coinciden. + +**Nunca declares `FastAPI-fastkit` como dependencia de ejecución** de un proyecto generado. La CLI es una herramienta de desarrollo; la lista de dependencias de una plantilla solo debe contener lo que importa la propia aplicación generada. ### Guía de escritura de archivos @@ -238,52 +243,23 @@ isort==5.12.0 mypy==1.7.1 ``` -#### 4. Escribir setup.py-tpl (legacy — opcional si existe pyproject) +#### 4. Anclas de rutas -Se conserva para plantillas legacy. Las plantillas nuevas no necesitan este archivo si traen `pyproject.toml-tpl`. +Las plantillas que incluyen un `main.py-tpl` o un módulo agregador de la API deben marcar dónde inserta código `fastkit addroute`: ```python -""" -Setup del paquete +from src.app.api import health -Proyecto creado con FastAPI-fastkit. -""" -from setuptools import find_packages, setup - -# Lista de dependencias (anotación de tipo requerida) -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", -] +# fastkit:imports -setup( - name="", - version="1.0.0", - description="[FastAPI-fastkit templated] ", # Marcador de identidad usado por 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", - ], -) +api_router = APIRouter() +api_router.include_router(health.router) + +# fastkit:routes ``` +Sin las anclas, `addroute` recurre a una inserción basada en AST que localiza por sí misma el bloque de imports y los registros de routers — así que una plantilla que no las tenga sigue funcionando, pero el resultado es menos predecible. Mantén los comentarios en su sitio al editar estos archivos. + #### 5. Escribir los archivos de pruebas ```python @@ -343,8 +319,17 @@ make inspect-template TEMPLATES="fastapi-your-template" # Validar con salida detallada python scripts/inspect-templates.py --templates "fastapi-your-template" --verbose + +# Ciclo local rápido: sin red, sin arrancar el servidor +python scripts/inspect-templates.py --templates "fastapi-your-template" \ + --offline --no-smoke + +# Comprobación completa antes de abrir un PR (añade mypy sobre el proyecto generado) +python scripts/inspect-templates.py --templates "fastapi-your-template" --mypy ``` +`--offline` omite las consultas de actualidad de dependencias en PyPI, `--no-smoke` omite el arranque del proyecto generado (el paso más lento) y `--mypy` comprueba los tipos del proyecto generado. Consulta [Aseguramiento de calidad de las plantillas](../reference/template-quality-assurance.md) para ver qué hace cada comprobación. + !!! note Cuando envíes un PR, el flujo de trabajo **Template PR Inspection** se ejecutará automáticamente y validará tus cambios en la plantilla. Recibirás comentarios directamente en tu PR. @@ -357,7 +342,7 @@ El inspector valida automáticamente lo siguiente: - [ ] Existe el directorio `tests/` - [ ] Existe el archivo `README.md-tpl` -- [ ] Existe al menos uno de `pyproject.toml-tpl` (preferido) o `setup.py-tpl` (legacy) +- [ ] Existe `pyproject.toml-tpl` (sin `setup.py-tpl` en las plantillas nuevas) #### ✅ Validación de extensiones @@ -369,7 +354,8 @@ El inspector valida automáticamente lo siguiente: - [ ] `fastapi` está declarado en al menos uno de: - [ ] `pyproject.toml-tpl` bajo `[project].dependencies` (preferido) - [ ] `requirements.txt-tpl` - - [ ] `setup.py-tpl` bajo `install_requires` +- [ ] `requirements.txt-tpl` y `[project].dependencies` no se contradicen +- [ ] `FastAPI-fastkit` **no** es una dependencia de ejecución #### ✅ Validación de la implementación de FastAPI @@ -382,6 +368,24 @@ El inspector valida automáticamente lo siguiente: - [ ] La instalación de dependencias es correcta - [ ] Todas las pruebas de pytest pasan +#### ✅ Prueba de humo + +- [ ] El proyecto generado arranca con uvicorn +- [ ] `/docs` responde +- [ ] `/health` responde + +El objetivo de uvicorn se resuelve a partir de `[tool.fastapi-fastkit].app_module` en el `pyproject.toml` generado, después de `app_module` en `template-config.yml` y, por último, de las ubicaciones convencionales de `main.py`. + +#### ✅ Coherencia de la configuración + +- [ ] `requires-python`, el `target-version` de black y el `python_version` de mypy indican Python 3.12 +- [ ] No hay divergencia de dependencias entre `requirements.txt-tpl` y `[project].dependencies` +- [ ] No hay autodependencia de `FastAPI-fastkit` + +#### ✅ Restos de marcadores + +- [ ] Ningún `` sobrevive a la generación del proyecto + #### ✅ Pruebas automatizadas de plantillas FastAPI-fastkit incluye **pruebas automatizadas de plantillas** que ejecutan pruebas integrales sobre todas las plantillas: diff --git a/docs/es/index.md b/docs/es/index.md index 1d695c4..178fed2 100644 --- a/docs/es/index.md +++ b/docs/es/index.md @@ -34,6 +34,8 @@ Está inspirado en el `SpringBoot initializer` y en el CLI `django-admin` de Pyt - **🔍 Aseguramiento de calidad automatizado de las plantillas**: pruebas automáticas semanales garantizan que todas las plantillas siguen funcionando y al día - **🚀 Múltiples plantillas de proyecto**: elige entre varias plantillas preconfiguradas para distintos casos de uso (async CRUD, Docker, PostgreSQL, etc.) - **📦 Soporte de varios gestores de paquetes**: elige tu gestor de paquetes Python preferido (pip, uv, pdm, poetry) para gestionar dependencias +- **🧾 Configuraciones de proyecto reproducibles**: guarda una sesión interactiva con `--save-config` y reprodúcela más tarde con `fastkit init --config` — sin prompts, el mismo proyecto +- **🏷️ Proyectos autodescriptivos**: cada proyecto generado registra su plantilla, preset, gestor de paquetes, entrypoint y funcionalidades seleccionadas en un bloque `[tool.fastapi-fastkit]` que `runserver` y `addroute` vuelven a leer ## Instalación @@ -123,7 +125,7 @@ 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 │ +│ ℹ Injected metadata into pyproject.toml │ ╰──────────────────────────────────────────────────────╯ ╭──────────────────────── Info ────────────────────────╮ │ ℹ Injected metadata into config file │ @@ -174,6 +176,16 @@ Installing dependencies... Este comando crea un nuevo espacio de trabajo para tu proyecto FastAPI e incluye también un entorno virtual de Python. +- Opciones principales: + - `--project-name`, `--author`, `--author-email`, `--description` + - `--package-manager` [pip|uv|pdm|poetry] + - `--config `: crea el proyecto a partir de un archivo de configuración guardado (`.json` / `.toml`, y `.yaml` cuando PyYAML está instalado), sin prompts + - `--save-config `: con `--interactive`, registra las respuestas para reutilizarlas después + - `--dry-run`: muestra los archivos y paquetes que se crearían, sin escribir nada + - `--no-venv`: omite la creación del entorno virtual (implica `--no-install`) + - `--no-install`: omite la instalación de dependencias + - `--yes` / `-y`: omite la confirmación "Overwrite these files?" al desplegar in situ + ### Crear un proyecto en modo interactivo ✨ ¡NUEVO! Para proyectos más complejos, usa el **modo interactivo** para construir tu aplicación FastAPI paso a paso con selección inteligente de funcionalidades: @@ -264,6 +276,30 @@ Select utilities (comma-separated numbers, e.g., 1,3,4): Select utilities: 1 +🪵 Logging Format +Select logging format: + 1. structured - JSON logs + X-Request-ID correlation middleware (stdlib only) + 2. None - Uvicorn's default plain-text logging + +Select logging format [2]: 1 + +🧬 Database Migrations +Select migration tool: + 1. Alembic - alembic.ini + async alembic/env.py + scripts/migrate.sh (recommended for SQL databases) + 2. None - No migration tooling + +Select migration tool [1]: 1 + +🧰 Developer Tooling +Select tooling to generate (comma-separated numbers, e.g., 1,3) + 1. ruff ruff + ruff-format config in pyproject.toml (replaces black/isort) + 2. pre-commit .pre-commit-config.yaml + 3. github-actions .github/workflows/test.yml (Python 3.12) + 4. devcontainer .devcontainer/devcontainer.json + 5. makefile Makefile with install/test/lint/format/run + +Your choice (or press Enter to skip): 1,2 + 🚀 Deployment Configuration Select deployment option: 1. Docker - Generate Dockerfile @@ -294,6 +330,9 @@ Enter custom package names (comma-separated, press Enter to skip): │ Monitoring │ Prometheus │ │ Testing │ Coverage │ │ Utilities │ CORS │ +│ Logging │ structured │ +│ Migrations │ Alembic │ +│ Tooling │ ruff, pre-commit │ │ Package Manager │ uv │ └─────────────────────┴───────────────────────────────────────────────────────────────────────────┘ @@ -350,6 +389,28 @@ El modo interactivo ofrece: - **Gestión inteligente de dependencias** con compatibilidad automática con pip - **Validación de funcionalidades** con avisos de cableado manual para las selecciones que el preset no puede conectar automáticamente - **Marcadores de identidad** en el `pyproject.toml` generado (marcador en `description` + tabla `[tool.fastapi-fastkit]`) para que `is_fastkit_project()` reconozca después los proyectos generados +- **Metadatos del proyecto** registrados en `[tool.fastapi-fastkit]` — plantilla, preset, gestor de paquetes, `app_module` y las `features` seleccionadas — que `fastkit runserver` y `fastkit addroute` vuelven a leer +- **Ejecuciones reproducibles**: `--save-config ` registra las respuestas, `--config ` las reproduce sin un solo prompt y `--dry-run` previsualiza el resultado sin escribir nada + +Ahora cada selección del catálogo genera código real y funcional, en lugar de +limitarse a instalar un paquete — las tareas en segundo plano +(Celery/Dramatiq), la caché con Redis, WebSocket, Pagination, el tracing con +OpenTelemetry y la autenticación OAuth2 / basada en sesiones incluyen un +módulo correspondiente y su cableado en `main.py`. Tres ejes nuevos completan +el catálogo: + +- **Logging**: `structured` añade salida estructurada con `logging` + `json` de la stdlib y un middleware de request-id — sin dependencias nuevas +- **Migraciones**: `Alembic` genera un entorno de migraciones asíncrono completo (`alembic.ini`, `alembic/env.py`, una revisión base, `scripts/migrate.sh`) para cualquier base de datos SQL seleccionada +- **Herramientas** (selección múltiple): `ruff`, `pre-commit`, `github-actions`, `devcontainer`, `makefile` — cada una genera su propio archivo de configuración; `ruff` además fusiona un bloque `[tool.ruff]` en `pyproject.toml` + +Cada proyecto generado obtiene también los endpoints `/health` y `/ready`, y +`--dry-run` lista exactamente el mismo conjunto de archivos que escribiría una +ejecución real, incluidos estos nuevos artefactos. `classic-layered` y +`domain-starter` conservan el `main.py` que trae la plantilla, así que la +validación de funcionalidades avisa cuando una selección de cualquiera de +estos ejes necesita cableado manual — consulta la +[matriz de presets / funcionalidades](reference/preset-feature-matrix.md) para +más detalles. ### Añadir una ruta nueva al proyecto FastAPI @@ -445,6 +506,12 @@ FastAPI template project will deploy at '~your-project-path~'
+- Opciones principales: + - `--project-name`, `--author`, `--author-email`, `--description` + - `--package-manager` [pip|uv|pdm|poetry] + - `--dry-run`, `--no-venv`, `--no-install`, `--yes`/`-y` — mismo significado que en `init` + - Se niega a ejecutarse si el directorio del proyecto de destino ya existe (salvo con `--dry-run`) + Para ver la lista de demos FastAPI disponibles, ejecuta:
@@ -463,18 +530,42 @@ $ fastkit list-templates │ │ PostgreSQL │ │ fastapi-default │ Simple FastAPI Project │ │ fastapi-single-module │ FastAPI Single Module Template │ +│ fastapi-auth-jwt │ FastAPI JWT Authentication │ +│ fastapi-sqlmodel │ FastAPI SQLModel │ +│ fastapi-llm-agent │ FastAPI LLM Agent │ └────────────────────────┴───────────────────────────────────────────────────────┘ ```
+Plantillas disponibles: + +| Plantilla | Descripción | Notas | +|---|---|---| +| [`fastapi-default`](https://github.com/bnbong/FastAPI-fastkit/blob/main/src/fastapi_fastkit/fastapi_project_template/fastapi-default/README.md-tpl) | Proyecto FastAPI simple con una estructura clásica por capas | Buena primera opción | +| [`fastapi-empty`](https://github.com/bnbong/FastAPI-fastkit/blob/main/src/fastapi_fastkit/fastapi_project_template/fastapi-empty/README.md-tpl) | Plantilla FastAPI mínima | Base del preset `minimal` | +| [`fastapi-single-module`](https://github.com/bnbong/FastAPI-fastkit/blob/main/src/fastapi_fastkit/fastapi_project_template/fastapi-single-module/README.md-tpl) | App FastAPI de un solo archivo | Base del preset `single-module` | +| [`fastapi-domain-starter`](https://github.com/bnbong/FastAPI-fastkit/blob/main/src/fastapi_fastkit/fastapi_project_template/fastapi-domain-starter/README.md-tpl) | Starter orientado a dominios, centrado en pyproject, para APIs de tamaño medio | Base del preset `domain-starter` | +| [`fastapi-auth-jwt`](https://github.com/bnbong/FastAPI-fastkit/blob/main/src/fastapi_fastkit/fastapi_project_template/fastapi-auth-jwt/README.md-tpl) | Autenticación JWT con rotación de refresh tokens, hash argon2 y scopes | Nueva en v1.4.0 | +| [`fastapi-sqlmodel`](https://github.com/bnbong/FastAPI-fastkit/blob/main/src/fastapi_fastkit/fastapi_project_template/fastapi-sqlmodel/README.md-tpl) | SQLModel asíncrono + migraciones con Alembic + CRUD genérico con paginación | Nueva en v1.4.0 | +| [`fastapi-llm-agent`](https://github.com/bnbong/FastAPI-fastkit/blob/main/src/fastapi_fastkit/fastapi_project_template/fastapi-llm-agent/README.md-tpl) | Agente de chat Claude con streaming (SSE) y bucle de llamadas a herramientas | Nueva en v1.4.0 | +| [`fastapi-mcp`](https://github.com/bnbong/FastAPI-fastkit/blob/main/src/fastapi_fastkit/fastapi_project_template/fastapi-mcp/README.md-tpl) | App FastAPI expuesta como servidor MCP | | +| [`fastapi-psql-orm`](https://github.com/bnbong/FastAPI-fastkit/blob/main/src/fastapi_fastkit/fastapi_project_template/fastapi-psql-orm/README.md-tpl) | API de gestión de items con PostgreSQL, SQLModel, Alembic y docker-compose | Requiere Docker | +| [`fastapi-custom-response`](https://github.com/bnbong/FastAPI-fastkit/blob/main/src/fastapi_fastkit/fastapi_project_template/fastapi-custom-response/README.md-tpl) | API de gestión de items con un envoltorio de respuesta personalizado, manejo de errores y paginación | | +| [`fastapi-async-crud`](https://github.com/bnbong/FastAPI-fastkit/blob/main/src/fastapi_fastkit/fastapi_project_template/fastapi-async-crud/README.md-tpl) | API asíncrona de gestión de items | **Obsoleta** — usa `fastapi-sqlmodel` | +| [`fastapi-dockerized`](https://github.com/bnbong/FastAPI-fastkit/blob/main/src/fastapi_fastkit/fastapi_project_template/fastapi-dockerized/README.md-tpl) | API de gestión de items dockerizada | **Obsoleta** — usa `fastapi-default` con la opción Docker de `init --interactive` | + +Las plantillas obsoletas siguen funcionando, pero ya no son puntos de partida recomendados. Consulta [¿Qué starter elegir?](user-guide/choosing-a-starter.md) para una guía de decisión. + ## Documentación Para guías completas e instrucciones detalladas, explora nuestra documentación: +- 🧭 **[¿Qué starter elegir?](user-guide/choosing-a-starter.md)** - Guía de decisión para principiantes sobre las plantillas de `startdemo` y los presets interactivos - 📚 **[Guía de usuario](user-guide/quick-start.md)** - Guías detalladas de instalación y uso - 🎯 **[Tutorial](tutorial/getting-started.md)** - Tutoriales paso a paso para principiantes - 📖 **[Referencia de la CLI](user-guide/cli-reference.md)** - Referencia completa de comandos +- 🧱 **[Matriz de presets de arquitectura](reference/preset-feature-matrix.md)** - Contrato por preset y por funcionalidad de la generación interactiva - 🔍 **[Aseguramiento de calidad de plantillas](reference/template-quality-assurance.md)** - Pruebas automatizadas y estándares de calidad ## 🚀 Tutoriales basados en plantillas @@ -486,16 +577,19 @@ Aprende FastAPI con casos prácticos usando nuestras plantillas ya preparadas: - **[Construir un servidor API básico](tutorial/basic-api-server.md)** - Crea tu primer servidor FastAPI con la plantilla `fastapi-default` - **[Construir una API CRUD asíncrona](tutorial/async-crud-api.md)** - Desarrolla una API async de alto rendimiento con la plantilla `fastapi-async-crud` - **[Proyecto orientado a dominios (Domain Starter)](tutorial/domain-starter.md)** - Construye una API mediana con la plantilla `fastapi-domain-starter`, la opción moderna recomendada +- **[Autenticación JWT](tutorial/auth-jwt.md)** - Añade cuentas reales con la plantilla `fastapi-auth-jwt`: refresh tokens rotatorios, hash argon2id y guards de roles y scopes ### 🗄️ Base de datos e infraestructura - **[Integrar con una base de datos](tutorial/database-integration.md)** - Usa PostgreSQL + SQLAlchemy con la plantilla `fastapi-psql-orm` -- **[Dockerizar y desplegar](tutorial/docker-deployment.md)** - Configura un entorno de despliegue de producción con la plantilla `fastapi-dockerized` +- **[Persistencia asíncrona con SQLModel](tutorial/sqlmodel.md)** - SQLModel asíncrono, migraciones con Alembic, una base CRUD genérica y listas paginadas con la plantilla `fastapi-sqlmodel` +- **[Dockerizar y desplegar](tutorial/docker-deployment.md)** - Configura un entorno de despliegue de producción con la plantilla `fastapi-dockerized` *(plantilla obsoleta — se mantiene para los usuarios existentes)* ### ⚡ Funcionalidades avanzadas - **[Manejo personalizado de respuestas y diseño avanzado de API](tutorial/custom-response-handling.md)** - Construye APIs de nivel empresarial con la plantilla `fastapi-custom-response` - **[Integrar con MCP](tutorial/mcp-integration.md)** - Crea un servidor API integrado con modelos de IA con la plantilla `fastapi-mcp` +- **[Agente LLM con streaming](tutorial/llm-agent.md)** - Construye un backend de chat Claude con streaming y un bucle real de llamadas a herramientas con la plantilla `fastapi-llm-agent` Cada tutorial ofrece: diff --git a/docs/es/reference/preset-feature-matrix.md b/docs/es/reference/preset-feature-matrix.md index 5769d9f..ffd02c0 100644 --- a/docs/es/reference/preset-feature-matrix.md +++ b/docs/es/reference/preset-feature-matrix.md @@ -29,12 +29,18 @@ Estas funcionalidades están soportadas en **todos** los presets: la instalació | Funcionalidad | `minimal` / `single-module` | `classic-layered` / `domain-starter` | |---|---|---| | **Base de datos** (PostgreSQL, MySQL, SQLite, MongoDB) | Genera el módulo de configuración **y** añade llamadas stub `await init_db()` en el `main.py` regenerado. | Genera el módulo de configuración en la ruta del preset. El `main.py` que viene en la plantilla se **conserva**, así que tienes que cablear `get_db()` en los routers manualmente. | -| **Autenticación** (JWT, FastAPI-Users, OAuth2, basada en sesiones) | Genera el módulo de configuración de auth. JWT también importa `HTTPBearer` en el `main.py` regenerado. | Genera el módulo de configuración de auth en la ruta del preset. No se añaden imports al `main.py` — cablea las dependencias manualmente. | -| **Tareas en segundo plano** (Celery, Dramatiq) | Los paquetes se instalan; por ahora no hay regeneración automática de `main.py`. | Igual. | -| **Caché** (Redis) | Los paquetes se instalan; por ahora no hay regeneración automática de `main.py`. | Igual. | +| **Autenticación** (JWT, FastAPI-Users, OAuth2, basada en sesiones) | Genera el módulo de configuración de auth; JWT importa `HTTPBearer`, y OAuth2 y la basada en sesiones añaden además su configuración de middleware en `main.py`. | Genera el módulo de configuración de auth en la ruta del preset. No se añaden imports al `main.py` — cablea las dependencias manualmente. | +| **Tareas en segundo plano** (Celery, Dramatiq) | Genera `worker.py` y `features/tasks.py`, y monta el router de tareas en el `main.py` regenerado. | Genera `worker.py` y `features/tasks.py`, pero el `main.py` que trae la plantilla se **conserva** — monta el router manualmente. | +| **Caché** (Redis) | Genera `features/cache.py` y lo monta e inicializa en el `main.py` regenerado. | Genera `features/cache.py`; móntalo manualmente. | +| **Utilidades: Pagination / WebSocket** | Genera `features/pagination.py` / `features/websocket.py` y monta el router en el `main.py` regenerado. | Genera el mismo módulo; móntalo manualmente. | +| **Monitorización: OpenTelemetry** | Sin archivo independiente — la configuración de tracing se añade directamente al `main.py` regenerado. | Sin archivo independiente. El `main.py` de la plantilla se conserva, así que el tracing **no queda cableado** — añade la configuración manualmente. | +| **Migraciones** (Alembic) | Genera `alembic.ini`, `alembic/env.py`, una revisión base y `scripts/migrate.sh` para cualquier base de datos SQL seleccionada — con independencia de la regeneración de `main.py`. | Igual — la generación de Alembic no depende de si `main.py` se conserva. | +| **Herramientas** (ruff, pre-commit, github-actions, devcontainer, makefile) | Genera los archivos de configuración correspondientes, con independencia de la regeneración de `main.py`. | Igual. | +| **Logging** (estructurado) | Genera `logging_config.py` y cablea el middleware de request-id en el `main.py` regenerado. | Genera `logging_config.py`; el `main.py` de la plantilla se conserva, así que cablea el middleware manualmente. | | **CORS** (utilidad) | Se añade `CORSMiddleware` al `main.py` regenerado con `allow_origins=['*']`. | **Ya cableado** en el `main.py` que trae la plantilla (condicional a `settings.all_cors_origins`). Actívalo definiendo `BACKEND_CORS_ORIGINS` en `.env` — no requiere cambios de código. | -| **Pruebas** (Basic / Coverage / Advanced) | Se genera `pytest.ini` en la raíz del proyecto. | Igual. | +| **Pruebas** (Basic / Coverage / Advanced) | Se genera `pytest.ini` en la raíz del proyecto; Advanced añade además `tests/factories.py` y `tests/test_factories.py`. | Igual. | | **Despliegue** (Docker, docker-compose) | Se escribe `Dockerfile` y/o `docker-compose.yml` en la raíz del proyecto. | Igual. | +| **`/health` / `/ready`** | Siempre presentes en el `main.py` regenerado. | Ya presentes en el `main.py` conservado que trae la plantilla. | ## Cuándo verás un aviso de "Preset compatibility" @@ -44,10 +50,15 @@ Para los presets que **conservan el `main.py` que trae la plantilla** (`classic- |---|---| | `CORS` (utilidad) | ❌ — ya cableado en el `main.py` de la plantilla. Solo rellena `BACKEND_CORS_ORIGINS` en `.env`. | | `Rate-Limiting` (utilidad) | ✅ — la configuración del limitador `slowapi` no se añade | -| `Prometheus` (monitorización) | ✅ — no se llama a `Instrumentator().instrument(app)` | +| `Pagination` / `WebSocket` (utilidad) | ✅ — el router generado (`features/pagination.py` / `features/websocket.py`) no se monta | +| `Celery` / `Dramatiq` (async_tasks) | ✅ — el router generado de `worker.py` / `features/tasks.py` no se monta | +| `Redis` (caché) | ✅ — el `features/cache.py` generado no se monta ni se inicializa | +| `Prometheus` / `OpenTelemetry` (monitorización) | ✅ — la configuración de instrumentación no se añade a `main.py` | +| `structured` (logging) | ✅ — se genera `logging_config.py`, pero el middleware de request-id no se añade a `main.py` | +| `Alembic` (migraciones), selecciones de `tooling` | ❌ — estos archivos se generan y se pueden usar sea cual sea el preset; nada depende de `main.py` | | Cualquier selección de base de datos / auth | ⚠️ — los archivos de configuración se generan, pero tienes que añadirlos con `Depends()` en tus routers | -Para los presets `minimal` y `single-module`, la regeneración dinámica de `main.py` se encarga automáticamente de CORS, rate limiting e instrumentación con Prometheus; no aparece ningún aviso. +Para los presets `minimal` y `single-module`, la regeneración dinámica de `main.py` se encarga automáticamente de todos los ejes anteriores; no aparece ningún aviso. ## Combinaciones no soportadas (juego seguro) diff --git a/docs/es/reference/template-quality-assurance.md b/docs/es/reference/template-quality-assurance.md index ee5d493..1ed8a04 100644 --- a/docs/es/reference/template-quality-assurance.md +++ b/docs/es/reference/template-quality-assurance.md @@ -21,6 +21,38 @@ Cada miércoles a medianoche (UTC), nuestro flujo de trabajo de GitHub Actions i - ✅ **Comprobación de dependencias** — confirma que FastAPI y las dependencias necesarias están bien declaradas - ✅ **Implementación de FastAPI** — verifica que las plantillas contienen una inicialización adecuada de la app FastAPI - ✅ **Ejecución de pruebas** — ejecuta las pruebas de la plantilla para confirmar que funcionan +- ✅ **Prueba de humo** — arranca el proyecto generado con uvicorn y sondea su superficie HTTP +- ✅ **Coherencia de la configuración** — versiones de Python fijadas, divergencia de dependencias, autodependencia +- ✅ **Restos de marcadores** — ningún `` sobrevive a la generación del proyecto +- ✅ **Actualidad de las dependencias** — avisa de las versiones fijadas que van por detrás de su última versión en PyPI + +## Qué hace cada comprobación + +La inspección genera un **proyecto real** a partir de la plantilla — copia, conversión de `-tpl`, inyección de metadatos — y después ejecuta cada comprobación sobre ese proyecto generado, no sobre el código fuente de la plantilla. Una comprobación que solo lee la plantilla no puede detectar un marcador que la generación no consiguió sustituir. + +### Prueba de humo + +El proyecto generado se inicia con uvicorn en un puerto libre, y el inspector consulta periódicamente hasta que el servidor responde o se agota el tiempo de espera. Una vez arrancado, se solicitan `/docs` y `/health` y se comprueban sus códigos de estado. Después se termina el proceso del servidor, recurriendo a `SIGKILL` si no se detiene. + +El objetivo de uvicorn se resuelve en este orden: `[tool.fastapi-fastkit].app_module` en el `pyproject.toml` generado, después `app_module` en el `template-config.yml` de la plantilla y, por último, las ubicaciones convencionales de `main.py`. Es el mismo contrato que usa `fastkit runserver`, así que una plantilla cuya prueba de humo arranca es una plantilla cuyo proyecto generado se ejecuta. + +Esta es la comprobación que detecta lo que el análisis estático no puede: un import que solo se resuelve en tiempo de ejecución, un `lifespan` que lanza una excepción, un campo de configuración sin valor por defecto. + +### Coherencia de la configuración + +Se validan tres cosas contra el `pyproject.toml-tpl` de la plantilla: + +- **Las versiones de Python fijadas coinciden y apuntan a 3.12** — `requires-python`, el `target-version` de black y el `python_version` de mypy. Una plantilla que formatea como `py311` mientras declara `>=3.12` es un bug esperando a llegar a un colaborador. +- **Sin divergencia de dependencias** — los paquetes declarados en `requirements.txt-tpl` y en `[project].dependencies` no deben contradecirse. Las plantillas incluyen ambos archivos por compatibilidad con los gestores de paquetes, y dos listas siempre acaban divergiendo si nada las comprueba. +- **Sin autodependencia** — un proyecto generado no debe declarar `FastAPI-fastkit` como dependencia de ejecución. La CLI es una herramienta de desarrollo; incluirla en cada app generada arrastra todo el árbol de dependencias de fastkit a proyectos que no tienen nada que ver. + +### Restos de marcadores + +Cada ``, `` y similares debe sustituirse durante la generación. Cualquier `` que siga presente en el proyecto generado es un error — significa que la inyección de metadatos no conoce ese archivo y que el usuario lo recibiría tal cual. + +### Actualidad de las dependencias + +Las versiones fijadas se comparan con la última versión publicada en PyPI y se informan como avisos, nunca como errores — una plantilla que va una versión menor por detrás es información, no un fallo. Es la única comprobación que accede a la red, y `--offline` la omite. ## Sistema automatizado de pruebas de plantillas @@ -140,6 +172,30 @@ $ python scripts/inspect-templates.py --verbose $ python scripts/inspect-templates.py --output my_results.json ``` +#### Flags de inspección + +| Flag | Efecto | Úsalo cuando | +|---|---|---| +| `--offline` | Omite todas las consultas de red (informe de actualidad de las dependencias) | No tienes red o quieres una ejecución rápida y aislada | +| `--no-smoke` | Omite el arranque del proyecto generado con uvicorn | Solo iteras sobre la estructura; la prueba de humo es el paso más lento | +| `--mypy` | Comprueba los tipos del proyecto generado con mypy | Antes de enviar una plantilla — es opcional porque es lento | +| `--templates` | Lista de plantillas a inspeccionar, separadas por comas | Trabajas en una sola plantilla | +| `--verbose` | Salida detallada | Depuras un fallo | +| `--output` | Ruta del archivo de resultados JSON | Quieres conservar varias ejecuciones una al lado de otra | + +Un ciclo local rápido sobre una sola plantilla: + +```console +$ python scripts/inspect-templates.py \ + --templates fastapi-sqlmodel --offline --no-smoke --verbose +``` + +...y la comprobación completa antes de abrir un PR: + +```console +$ python scripts/inspect-templates.py --templates fastapi-sqlmodel --mypy +``` + ### Usar los comandos del Makefile ```console @@ -192,10 +248,11 @@ Para que una plantilla pase la inspección, debe cumplir estos requisitos: - Debe contener un directorio `src/` con archivos fuente Python - Los archivos Python deben usar la extensión `.py-tpl` - Debe incluir un directorio `tests/` y un archivo `README.md-tpl` -- Debe incluir **al menos uno** de los archivos de metadatos: - - `pyproject.toml-tpl` (preferido, PEP 621), o - - `setup.py-tpl` (legacy, todavía aceptado) -- `requirements.txt-tpl` es opcional cuando `pyproject.toml-tpl` declara `[project].dependencies` +- Debe incluir `pyproject.toml-tpl` (PEP 621). Las plantillas incluidas priorizan pyproject y ya no traen `setup.py-tpl`; la inspección todavía lo acepta para que las plantillas de terceros sigan funcionando, pero las plantillas nuevas no deben añadirlo. +- `requirements.txt-tpl` es opcional cuando `pyproject.toml-tpl` declara `[project].dependencies` — pero, cuando ambos están presentes, deben coincidir (ver *Coherencia de la configuración*) +- Debe apuntar a Python 3.12 de forma coherente: `requires-python = ">=3.12"`, black `target-version = ["py312"]`, mypy `python_version = "3.12"` y `python:3.12-slim` para cualquier imagen base de Docker +- No debe declarar `FastAPI-fastkit` como dependencia de ejecución +- No debe dejar ningún `` sin sustituir después de la generación ### Requisitos de FastAPI - Debe contener la inicialización de la app FastAPI diff --git a/docs/es/reference/translation-status.md b/docs/es/reference/translation-status.md index 3c897d5..7c3dfcc 100644 --- a/docs/es/reference/translation-status.md +++ b/docs/es/reference/translation-status.md @@ -18,15 +18,15 @@ Los números siguientes cuentan páginas Markdown presentes en el árbol de cada | Idioma | Estado | Páginas Markdown | Notas | |---|---|---:|---| -| 🇬🇧 Inglés (`en`) | ✅ Fuente de verdad | 26 / 26 | Autoritativa. | -| 🇰🇷 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`) | ✅ 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. | +| 🇬🇧 Inglés (`en`) | ✅ Fuente de verdad | 29 / 29 | Autoritativa. | +| 🇰🇷 Coreano (`ko`) | ✅ Completo | 29 / 29 | Phases 1-3 completadas. Resincronizado con las fuentes inglesas de v1.4.1 el 2026-10-11: los tres tutoriales de v1.4.0 (`tutorial/auth-jwt.md`, `tutorial/sqlmodel.md`, `tutorial/llm-agent.md`) están traducidos y las páginas que se habían desviado desde su traducción original se han vuelto a alinear. `docs/ko/changelog.md` reutiliza intencionadamente el `CHANGELOG.md` canónico en inglés. | +| 🇯🇵 Japonés (`ja`) | ✅ Completo | 29 / 29 | Phases 1-3 completadas. Resincronizado con las fuentes inglesas de v1.4.1 el 2026-10-11: los tres tutoriales de v1.4.0 (`tutorial/auth-jwt.md`, `tutorial/sqlmodel.md`, `tutorial/llm-agent.md`) están traducidos y las páginas que se habían desviado desde su traducción original se han vuelto a alinear. `docs/ja/changelog.md` reutiliza intencionadamente el `CHANGELOG.md` canónico en inglés. | +| 🇪🇸 Español (`es`) | ✅ Completo | 29 / 29 | Phases 1-3 completadas. Resincronizado con las fuentes inglesas de v1.4.1 el 2026-10-11: los tres tutoriales de v1.4.0 (`tutorial/auth-jwt.md`, `tutorial/sqlmodel.md`, `tutorial/llm-agent.md`) están traducidos y las páginas que se habían desviado desde su traducción original se han vuelto a alinear. `docs/es/changelog.md` reutiliza intencionadamente el `CHANGELOG.md` canónico en inglés. | +| 🇨🇳 Chino (`zh`) | ✅ Completo | 29 / 29 | Phases 1-3 completadas. Resincronizado con las fuentes inglesas de v1.4.1 el 2026-10-11: los tres tutoriales de v1.4.0 (`tutorial/auth-jwt.md`, `tutorial/sqlmodel.md`, `tutorial/llm-agent.md`) están traducidos y las páginas que se habían desviado desde su traducción original se han vuelto a alinear. `docs/zh/changelog.md` reutiliza intencionadamente el `CHANGELOG.md` canónico en inglés. | +| 🇫🇷 Francés (`fr`) | ✅ Completo | 29 / 29 | Phases 1-3 completadas. Resincronizado con las fuentes inglesas de v1.4.1 el 2026-10-11: los tres tutoriales de v1.4.0 (`tutorial/auth-jwt.md`, `tutorial/sqlmodel.md`, `tutorial/llm-agent.md`) están traducidos y las páginas que se habían desviado desde su traducción original se han vuelto a alinear. `docs/fr/changelog.md` reutiliza intencionadamente el `CHANGELOG.md` canónico en inglés. | +| 🇩🇪 Alemán (`de`) | ✅ Completo | 29 / 29 | Phases 1-3 completadas. Resincronizado con las fuentes inglesas de v1.4.1 el 2026-10-11: los tres tutoriales de v1.4.0 (`tutorial/auth-jwt.md`, `tutorial/sqlmodel.md`, `tutorial/llm-agent.md`) están traducidos y las páginas que se habían desviado desde su traducción original se han vuelto a alinear. `docs/de/changelog.md` reutiliza intencionadamente el `CHANGELOG.md` canónico en inglés. | -*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: +*Instantánea verificada el 2026-10-11, tras la release v1.4.1. Los seis idiomas traducidos se pusieron a la par con las fuentes inglesas: se tradujeron los tres tutoriales añadidos en v1.4.0 y se resincronizó cada página que se había desviado desde su traducción original. Las traducciones todavía pueden quedarse atrás dentro de una release — si una página traducida no coincide con la inglesa, confía en la inglesa.* 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 @@ -39,6 +39,7 @@ Si el recuento difiere de la tabla, la tabla está obsoleta — actualízala (o Leyenda: - ✅ **Fuente de verdad** — el idioma contra el que se escribe primero. +- ✅ **Completo** — cada página inglesa tiene su equivalente traducido. Algunas páginas concretas pueden quedarse atrás respecto a una actualización inglesa dentro de una release. - 🟡 **Parcial** — hay algunas páginas traducidas; las que faltan muestran la versión en inglés. - 🔴 **Esqueleto** — la entrada del selector de idiomas existe, pero todavía no hay páginas traducidas en el repositorio. El sitio renderiza contenido en inglés bajo las etiquetas de navegación traducidas. diff --git a/docs/es/tutorial/auth-jwt.md b/docs/es/tutorial/auth-jwt.md new file mode 100644 index 0000000..d4bc9e0 --- /dev/null +++ b/docs/es/tutorial/auth-jwt.md @@ -0,0 +1,319 @@ +# Autenticación JWT con `fastapi-auth-jwt` + +Construye una API con cuentas reales desde el primer commit. Este tutorial +recorre la plantilla `fastapi-auth-jwt` de principio a fin: cómo se emiten y +rotan los pares de tokens, por qué se rechaza un refresh token reutilizado, +cómo se conectan los guards de roles y scopes, y qué cambia cuando dejas +SQLite. + +## Qué vas a aprender + +- Generar un proyecto con `fastkit startdemo fastapi-auth-jwt` +- La diferencia entre el access token y el refresh token, y por qué importa + la rotación +- Cómo `get_current_user`, `get_current_active_superuser` y + `require_scopes(...)` protegen una ruta +- Dónde se hace el hash de contraseñas y por qué se usa argon2id +- Pasar del archivo SQLite incluido a PostgreSQL con Alembic + +## Requisitos previos + +- Python 3.12+ +- FastAPI-fastkit instalado (`pip install fastapi-fastkit`) +- Soltura con las dependencias de FastAPI y los esquemas pydantic + +Si nunca has construido una app FastAPI, empieza por +[Construir un servidor API básico](basic-api-server.md) y vuelve después — +esta plantilla da por sabidos los conceptos básicos. + +## Paso 1: Generar el proyecto + +```console +$ fastkit startdemo fastapi-auth-jwt +Enter the project name: accounts-api +Enter the author name: Developer Kim +Enter the author email: developer@example.com +Enter the project description: Accounts and sessions service +Select package manager (pip, uv, pdm, poetry) [uv]: uv +Do you want to proceed with project creation? [y/N]: y +``` + +Después, ejecútalo: + +```console +$ cd accounts-api +$ bash scripts/run-server.sh # o: uvicorn src.app.main:app --reload +``` + +La app arranca sobre SQLite (`sqlite:///./app.db`) y crea sus tablas al +iniciar, así que no hay nada que instalar ni configurar antes. La +documentación está en . + +!!! tip "Previsualiza antes de comprometerte" + `fastkit startdemo fastapi-auth-jwt --dry-run` muestra el árbol de archivos + y la lista de paquetes sin escribir nada — útil para ver qué vas a + obtener. + +## Paso 2: El árbol generado + +``` +accounts-api/ +├── pyproject.toml # Metadatos PEP 621 + [tool.fastapi-fastkit] +├── requirements.txt +├── alembic.ini +├── Dockerfile +├── .env # SECRET_KEY, DATABASE_URL, duración de los tokens +├── scripts/ +│ ├── format.sh lint.sh run-server.sh test.sh +├── src/ +│ └── app/ +│ ├── main.py # FastAPI() + lifespan + CORS + api_router +│ ├── core/ +│ │ ├── config.py # pydantic-settings +│ │ └── security.py # hash argon2 + codificación/decodificación JWT +│ ├── db/session.py # engine + dependencia get_session +│ ├── api/ +│ │ ├── router.py # agrega health + el router de cada dominio +│ │ ├── health.py # GET /health +│ │ └── deps.py # sesión, usuario actual, guards de rol/scope +│ ├── domains/ +│ │ ├── auth/ # login, rotación de refresh, logout +│ │ └── users/ # registro y perfiles +│ └── alembic/ # entorno de migraciones + versions/ +└── tests/ + ├── conftest.py test_health.py test_auth.py test_users.py +``` + +La estructura es la forma de `domain-starter`: cada dominio tiene su propio +`router.py` (transporte), `service.py` (lógica de negocio), `repository.py` +(acceso a datos), `models.py` (tablas) y `schemas.py` (E/S de la API). Lo +relativo a la autenticación vive en `auth/`, lo relativo a las cuentas en +`users/`, y ninguno de los dos accede a los detalles internos del otro. + +## Paso 3: Registrarse, iniciar sesión y llamar a una ruta protegida + +```console +# 1. registro +$ curl -X POST http://127.0.0.1:8000/api/v1/auth/register \ + -H 'Content-Type: application/json' \ + -d '{"email": "alice@example.com", "password": "s3cret-password"}' + +# 2. inicio de sesión — fíjate en la codificación de formulario: es el flujo de contraseña de OAuth2 +$ curl -X POST http://127.0.0.1:8000/api/v1/auth/login \ + -d 'username=alice@example.com&password=s3cret-password' +{"access_token": "...", "refresh_token": "...", "token_type": "bearer"} + +# 3. llamar a una ruta protegida +$ curl http://127.0.0.1:8000/api/v1/users/me \ + -H "Authorization: Bearer $ACCESS_TOKEN" +``` + +El botón "Authorize" de Swagger sigue el mismo flujo, así que puedes probar +las rutas protegidas desde `/docs` sin salir del navegador. + +La superficie completa: + +| Método | Endpoint | Auth | Descripción | +|--------|-----------------------------|-------------|------------------------------------------| +| GET | `/api/v1/health` | — | Probe de liveness | +| POST | `/api/v1/auth/register` | — | Crear una cuenta | +| POST | `/api/v1/auth/login` | — | Formulario de contraseña OAuth2 → par de tokens | +| POST | `/api/v1/auth/refresh` | refresh tok | Rotar a un par de tokens nuevo | +| POST | `/api/v1/auth/logout` | refresh tok | Revocar un refresh token | +| POST | `/api/v1/auth/logout-all` | access tok | Revocar todas las sesiones del usuario | +| GET | `/api/v1/users/me` | access tok | Perfil del usuario actual | +| PATCH | `/api/v1/users/me` | access tok | Actualizar el perfil del usuario actual | +| GET | `/api/v1/users/me/scopes` | scope `me` | Ruta de ejemplo protegida por scope | +| GET | `/api/v1/users` | superuser | Listado de usuarios protegido por rol | + +## Paso 4: Dos tokens, y por qué + +Un inicio de sesión devuelve un **par**: + +- El **access token** es de corta duración (`ACCESS_TOKEN_EXPIRE_MINUTES`, + 30 por defecto) y viaja en cada petición. Como dura poco, uno que se + filtre deja de funcionar enseguida. +- El **refresh token** es de larga duración (`REFRESH_TOKEN_EXPIRE_DAYS`, 14 + por defecto) y sirve exactamente para una cosa: obtener un par nuevo. + +Nada de lo anterior es inusual. Lo que distingue a esta plantilla del +fragmento JWT típico es que los refresh tokens se **registran y rotan**. +Cada refresh token emitido recibe un claim `jti`, y ese `jti` se guarda en +la base de datos: + +```python +def issue_token_pair(self, user: User) -> TokenPair: + scopes = user.scope_list + access_token, _, _ = create_access_token(str(user.id), scopes) + refresh_token, jti, expires_at = create_refresh_token(str(user.id), scopes) + self.refresh_tokens.add(jti, user.id or 0, expires_at) + return TokenPair(access_token=access_token, refresh_token=refresh_token) +``` + +Al refrescar, el token presentado se revoca **antes** de emitir el par +nuevo: + +```python +def rotate(self, refresh_token: str) -> TokenPair: + payload = decode_token(refresh_token, REFRESH_TOKEN_TYPE) + record = self.refresh_tokens.get_by_jti(str(payload.get("jti"))) + if record is None or record.revoked: + raise TokenError("refresh token is no longer valid") + ... + self.refresh_tokens.revoke(record) + return self.issue_token_pair(user) +``` + +Pruébalo: refresca una vez y luego vuelve a enviar el refresh token *antiguo*. + +```console +$ curl -X POST http://127.0.0.1:8000/api/v1/auth/refresh \ + -H 'Content-Type: application/json' \ + -d "{\"refresh_token\": \"$REFRESH_TOKEN\"}" +# → par nuevo + +$ curl -X POST http://127.0.0.1:8000/api/v1/auth/refresh \ + -H 'Content-Type: application/json' \ + -d "{\"refresh_token\": \"$REFRESH_TOKEN\"}" +# → 401: el token dejó de valer en el momento en que se usó +``` + +Esa única propiedad convierte un refresh token robado, que sería una puerta +trasera permanente, en un fallo detectable de un solo uso. + +El logout funciona igual, y es lo otro que las configuraciones JWT simples +no suelen poder hacer: `POST /auth/logout` revoca un token y +`POST /auth/logout-all` revoca todos los refresh tokens del usuario. + +## Paso 5: Contraseñas + +El hash vive en `src/app/core/security.py` y pasa por `pwdlib` con +**argon2id** — el valor por defecto recomendado actualmente, no una +configuración heredada de bcrypt. Ningún router ni service ve nunca una +contraseña en texto plano más allá del momento en que se verifica, y +`hashed_password` nunca forma parte de un esquema de respuesta. + +Aquí no tienes que configurar nada. Lo único que *sí* debes configurar es la +clave de firma. + +## Paso 6: Roles y scopes + +Se incluyen tres guards como dependencias de FastAPI: + +```python +CurrentUser = Annotated[User, Depends(get_current_user)] +``` + +- **`get_current_user`** decodifica el access token bearer, carga el usuario + y rechaza las cuentas inactivas con `403`. Cualquier token malformado, + caducado o que apunte a un usuario eliminado devuelve `401`. +- **`get_current_active_superuser`** añade encima una comprobación de + `is_superuser`. `GET /api/v1/users` la usa. +- **`require_scopes(...)`** construye una dependencia que exige scopes + concretos: + +```python +from fastapi import Depends +from src.app.api.deps import require_scopes + +@router.get("/reports", dependencies=[Depends(require_scopes("reports"))]) +def read_reports() -> list[str]: + ... +``` + +Los scopes se guardan en la fila del usuario como una cadena separada por +comas (`scopes: str`, leída a través de la propiedad `scope_list`) y se +copian en cada token emitido. Es, a propósito, lo más simple que funciona +para un starter; cuando tu modelo de permisos se quede pequeño para una +cadena, sustituye la columna por una tabla de roles y mantén sin cambios las +firmas de las dependencias — no hay que mover nada en los routers. + +## Paso 7: Añadir un dominio protegido + +Para añadir, por ejemplo, un dominio `orders` al que solo puedan acceder los +usuarios autenticados: + +1. Crea `src/app/domains/orders/` imitando `users/` + (`models.py`, `schemas.py`, `repository.py`, `service.py`, `router.py`). +2. Recibe `CurrentUser` como parámetro en las rutas que necesitan una + cuenta, o añade `Depends(require_scopes("orders"))` al router. +3. Registra el router en `src/app/api/router.py`, encima del ancla + `# fastkit:routes`. `fastkit addroute` escribe en esas mismas anclas, así + que las rutas escritas a mano y las generadas quedan en un mismo sitio. +4. Genera una migración: `alembic revision --autogenerate -m "add orders"`. +5. Replica `tests/test_users.py` para los nuevos endpoints. + +## Paso 8: SQLite ahora, PostgreSQL después + +El `DATABASE_URL` por defecto es un archivo SQLite local y la app llama a +`create_db_and_tables()` al arrancar; por eso tanto `pytest` como `uvicorn` +funcionan sin ninguna preparación. Para cualquier cosa más allá del +desarrollo local, deja que Alembic se encargue del esquema: + +```console +$ export DATABASE_URL='postgresql+psycopg://postgres:postgres@localhost:5432/app_db' +$ alembic upgrade head +``` + +Después de un cambio en los modelos: + +```console +$ alembic revision --autogenerate -m "add profile fields" +``` + +## Paso 9: Antes de desplegar + +Dos ajustes de `.env` deciden si esto es una demo o un servicio real: + +- **`SECRET_KEY`** firma cada token. Si no lo defines, se genera una clave + aleatoria por proceso — lo que cierra la sesión de todo el mundo en cada + reinicio y hace que varios workers discrepen sobre qué tokens son válidos. + Genera una y fíjala: + + ```console + $ python -c "import secrets; print(secrets.token_urlsafe(32))" + ``` + +- **`DATABASE_URL`** selecciona la base de datos. El SQLite por defecto no + es un almacenamiento de producción. + +Opcionalmente, define `FIRST_SUPERUSER_EMAIL` y `FIRST_SUPERUSER_PASSWORD` +para que se cree un superusuario al arrancar, y `BACKEND_CORS_ORIGINS` con +una lista separada por comas de orígenes del front-end. + +## Paso 10: Los tests + +```console +$ bash scripts/test.sh # o: pytest +``` + +La suite ejecuta `httpx.AsyncClient` contra una base de datos SQLite en +memoria y cubre todo el camino feliz — registro → login → me → refresh → +logout — además de las rutas de rechazo que de verdad importan: contraseña +incorrecta, token malformado, token caducado y el refresh token reutilizado +del Paso 4. Cuando amplíes las reglas de autenticación, amplía primero esos +tests de rechazo; son los que detectan un guard roto. + +## Resumen + +- **Generación**: `fastkit startdemo fastapi-auth-jwt` → + `bash scripts/run-server.sh` → documentación en `/docs`. +- **Tokens**: access token de corta duración para las peticiones y refresh + token rotatorio registrado por `jti`, de modo que las reutilizaciones + fallan y el logout es real. +- **Guards**: `get_current_user`, `get_current_active_superuser` y + `require_scopes(...)` como dependencias normales. +- **Almacenamiento**: SQLite sin coste, PostgreSQL definiendo `DATABASE_URL` + y ejecutando `alembic upgrade head`. +- **Antes de desplegar**: fija `SECRET_KEY` y apunta `DATABASE_URL` a una + base de datos real. + +## Siguientes pasos + +- [Persistencia con SQLModel](sqlmodel.md) — la misma estructura de dominios + centrada en el acceso asíncrono a datos, las migraciones y el CRUD + genérico. +- [Proyecto orientado a dominios](domain-starter.md) — la estructura sobre la + que se construye esta plantilla, sin la maquinaria de autenticación. +- [¿Qué starter elegir?](../user-guide/choosing-a-starter.md) — + cómo se compara esta plantilla con las demás. diff --git a/docs/es/tutorial/llm-agent.md b/docs/es/tutorial/llm-agent.md new file mode 100644 index 0000000..ee75960 --- /dev/null +++ b/docs/es/tutorial/llm-agent.md @@ -0,0 +1,277 @@ +# Agente LLM con streaming con `fastapi-llm-agent` + +Construye un backend de chat que transmite los tokens a medida que se +generan y que puede *hacer* cosas de verdad — llamar a herramientas, leer +sus resultados y seguir adelante. Este tutorial recorre la plantilla +`fastapi-llm-agent`: el protocolo de eventos SSE, el bucle de llamadas a +herramientas y su límite de iteraciones, la memoria de conversación detrás +de una interfaz y una suite de tests que nunca toca la red. + +## Qué vas a aprender + +- Generar un proyecto con `fastkit startdemo fastapi-llm-agent` +- Los tipos de eventos SSE que un cliente tiene que manejar y qué significa + cada uno +- Cómo funciona, paso a paso, un turno del bucle de llamadas a herramientas +- Añadir tu propia herramienta +- Sustituir el almacén de conversaciones en proceso por uno real +- Por qué los tests son deterministas y gratuitos de ejecutar + +## Requisitos previos + +- Python 3.12+ +- FastAPI-fastkit instalado (`pip install fastapi-fastkit`) +- Una API key de Anthropic — solo para hablar con un modelo real. La suite + de tests generada se ejecuta sin ella. +- Familiaridad con `async`/`await` y con los generadores asíncronos + +## Paso 1: Generar y configurar + +```console +$ fastkit startdemo fastapi-llm-agent +Enter the project name: support-agent +Enter the author name: Developer Kim +Enter the author email: developer@example.com +Enter the project description: Streaming support assistant +Select package manager (pip, uv, pdm, poetry) [uv]: uv +Do you want to proceed with project creation? [y/N]: y +``` + +Pon tu clave en `.env` antes de la primera ejecución: + +```bash +ANTHROPIC_API_KEY=sk-ant-... +LLM_MODEL=claude-sonnet-5 +LLM_MAX_TOKENS=4096 +MAX_TOOL_ITERATIONS=5 +``` + +pydantic-settings lee la clave y se la pasa al cliente al construirlo. +Nunca se escribe en el código fuente ni se incrusta en la imagen de Docker. + +```console +$ cd support-agent +$ bash scripts/run-server.sh +$ bash scripts/chat.sh "What is 12 * 34?" +``` + +!!! tip "Usar un gateway en lugar de la API de Anthropic" + Define `LLM_BASE_URL` (y `LLM_AUTH_TOKEN` si el gateway usa + autenticación bearer) para apuntar a LiteLLM, a un proxy interno o a un + servidor local. El formato de transmisión sigue siendo la Messages API de + Anthropic; traducir para otros proveedores es tarea del gateway, no de + la app. + +## Paso 2: El árbol generado + +``` +support-agent/ +├── pyproject.toml # Metadatos PEP 621 + [tool.fastapi-fastkit] +├── requirements.txt +├── Dockerfile +├── .env +├── scripts/ +│ ├── chat.sh # envía un mensaje a un servidor en ejecución +│ ├── format.sh lint.sh run-server.sh test.sh +├── src/ +│ └── app/ +│ ├── main.py # app FastAPI + lifespan +│ ├── core/config.py # pydantic-settings (aquí vive la API key) +│ ├── llm/ +│ │ ├── client.py # fábrica de AsyncAnthropic +│ │ ├── tools.py # esquemas de herramientas + handlers +│ │ └── agent.py # agente con streaming + bucle de herramientas +│ ├── memory/ +│ │ ├── base.py # interfaz ConversationStore +│ │ └── in_memory.py # implementación local al proceso +│ ├── schemas/chat.py # cuerpos de petición + envoltorio de eventos SSE +│ └── api/ +│ ├── router.py # agrega health + chat +│ ├── deps.py # dependencias de store / agente +│ ├── health.py # GET /health +│ └── chat.py # POST /chat (SSE), historial de conversación +└── tests/ + ├── conftest.py # cliente Anthropic falso con guion + ├── test_health.py test_chat.py test_tools.py test_memory.py +``` + +Conviene fijarse desde el principio en tres fronteras: el **cliente** se +crea con una fábrica (para que los tests puedan sustituirlo), la +**memoria** es una interfaz con una implementación trivial (para que +producción pueda sustituirla) y las **herramientas** son una lista de +esquemas más un diccionario de handlers (para que añadir una solo toque un +archivo). + +## Paso 3: Los endpoints + +| Método | Endpoint | Descripción | +|--------|------------------------------|---------------------------------| +| GET | `/api/v1/health` | Probe de liveness | +| POST | `/api/v1/chat` | Transmite una respuesta por SSE | +| GET | `/api/v1/conversations/{id}` | Leer el historial guardado | +| DELETE | `/api/v1/conversations/{id}` | Olvidar una conversación | + +`POST /api/v1/chat` recibe `{"message": "...", "conversation_id": "..."}`. +Omite `conversation_id` para empezar una conversación nueva y lee el id +asignado en el evento final `done`. + +## Paso 4: El protocolo de streaming + +Cada mensaje SSE lleva su tipo en el campo `event:` y un envoltorio JSON en +`data:`. Un cliente tiene que manejar cinco de ellos además de los errores: + +| Evento | Campos | Significado | +|---------------|---------------------------------|----------------------------------------| +| `token` | `text` | Un fragmento de la respuesta del asistente | +| `tool_use` | `tool_name`, `tool_input` | Claude ha pedido una herramienta | +| `tool_result` | `tool_name`, `tool_output` | La herramienta se ha ejecutado | +| `usage` | `input_tokens`, `output_tokens` | Consumo de tokens por turno | +| `done` | `conversation_id` | El turno ha terminado | +| `error` | `detail` | Algo falló a mitad del stream | + +`tool_use` y `tool_result` no son decoración — son lo que permite que una +interfaz muestre "buscando…" en lugar de una pausa sin explicación mientras +se ejecuta una herramienta. + +El uso de tokens también se escribe en el logger `app.agent` en cada turno, +así que el consumo es visible en los logs del servidor sin instrumentar un +cliente. + +## Paso 5: Un turno del bucle + +`ChatAgent.stream` en `src/app/llm/agent.py` es todo el motor. Un turno +transcurre así: + +1. Cargar el historial de la conversación desde el store. +2. Abrir una petición en streaming con el historial, el nuevo mensaje del + usuario y los esquemas de las herramientas. +3. Emitir cada fragmento de texto como evento `token` en cuanto llega. +4. Cuando termina el stream, leer el mensaje final; emitir un evento + `usage` y registrar en el log el recuento de tokens. +5. **Si `stop_reason != "tool_use"`,** el asistente ha terminado: guardar + el turno, emitir `done` y volver. +6. **En caso contrario,** para cada bloque `tool_use`: emitir `tool_use`, + ejecutar el handler, emitir `tool_result` y recoger la salida. +7. Enviar todos los resultados de herramientas de vuelta en un único + mensaje `user` — es lo que exige la Messages API — y repetir desde el + paso 2. + +El bucle está limitado por `MAX_TOOL_ITERATIONS` (5 por defecto). Si Claude +sigue pidiendo herramientas más allá del límite, el turno se guarda, se +registra una advertencia y se emite un evento `error` en lugar de dejar el +servidor dando vueltas: + +```python +yield AgentEvent( + type="error", + detail=( + "The assistant exceeded the tool-call limit of " + f"{self._max_tool_iterations} iterations." + ), +) +``` + +Un límite así no es opcional en un bucle de agente. Sin él, un modelo que +malinterpreta el resultado de una herramienta puede generarte costes de +forma indefinida. + +## Paso 6: Las herramientas incluidas + +| Herramienta | Propósito | +|----------------|-----------------------------------------------------------------| +| `calculator` | Aritmética, analizada como AST con lista blanca — no `eval` | +| `current_time` | Hora actual en una zona horaria IANA | + +La `calculator` merece una segunda mirada. Analiza la expresión con `ast` y +solo recorre tipos de nodo de una lista blanca, así que un argumento de +herramienta que viene de un modelo no puede ejecutar Python arbitrario. +Cualquier herramienta que acepte entrada generada por el modelo necesita la +misma disciplina: **la salida del modelo es entrada no confiable.** + +### Añadir una herramienta + +Todo vive en `src/app/llm/tools.py`: + +1. Añade un esquema a `TOOL_SCHEMAS` — `name`, `description` y un + `input_schema` en JSON Schema. +2. Registra el handler en `TOOL_HANDLERS` con el mismo nombre. + +Dos cosas que hay que hacer bien: + +- **La descripción es toda la instrucción que recibe Claude** sobre cuándo + recurrir a la herramienta. Es prompt engineering, no documentación — las + descripciones vagas producen herramientas que nunca se llaman o que se + llaman constantemente. +- **Los handlers deben devolver una cadena y convertir sus propios fallos + en un mensaje legible** en lugar de lanzar una excepción. El bucle + devuelve ese texto directamente al modelo, así que "no city named X was + found" permite que el modelo se recupere, mientras que una excepción + termina el turno. + +## Paso 7: Memoria de conversación + +`ConversationStore` (`src/app/memory/base.py`) es la interfaz; el +`in_memory.py` incluido la implementa con un diccionario. Eso sirve para un +único proceso y es incorrecto para cualquier otra cosa — un reinicio lo +olvida todo y dos workers discrepan sobre el historial. + +Para hacerlo real: implementa la misma interfaz sobre Redis o una base de +datos y devuélvela desde `get_store()` en `src/app/api/deps.py`. No cambia +nada en el agente ni en los routers, porque ninguno de los dos nombra nunca +la clase concreta. + +## Paso 8: Los tests + +```console +$ bash scripts/test.sh # o: pytest +``` + +`tests/conftest.py` reproduce turnos con guion a través de un objeto falso +que imita `AsyncAnthropic.messages.stream`. Así, la suite es determinista, +se ejecuta sin conexión, no cuesta nada y aun así cubre los caminos que +importan: el camino de streaming y el bucle completo de herramientas, +incluido un turno en el que se llama a una herramienta y después el modelo +responde. + +Este es el patrón que conviene mantener a medida que amplíes el agente. +Probar una integración LLM contra el modelo real te da una suite lenta, +inestable y cara que, aun así, no puede afirmar lo que dirá el modelo. +Probar el *bucle* contra un cliente con guion afirma exactamente lo que tú +escribiste. + +## Paso 9: Puntos de extensión + +- **Historial persistente** — implementa `ConversationStore` sobre Redis o + una base de datos (Paso 7). +- **RAG** — la recuperación queda deliberadamente fuera del alcance de la + plantilla. El punto de unión natural es `ChatAgent.stream`: obtén el + contexto antes de la primera petición y antepónlo al system prompt, o + expón tu índice como una herramienta más y deja que el modelo decida + cuándo buscar. +- **Autenticación y rate limiting** — `POST /chat` viene sin autenticación. + Añade una dependencia al router antes de exponerlo públicamente; el + [tutorial de Autenticación JWT](auth-jwt.md) tiene los guards que + reutilizarías. + +## Resumen + +- **Generación**: `fastkit startdemo fastapi-llm-agent`, define + `ANTHROPIC_API_KEY`, `bash scripts/run-server.sh`. +- **Streaming**: SSE con eventos `token` / `tool_use` / `tool_result` / + `usage` / `done` / `error`. +- **Bucle**: stream → llamadas a herramientas → resultados de vuelta en un + único mensaje de usuario → repetir, limitado por `MAX_TOOL_ITERATIONS`. +- **Herramientas**: un esquema en `TOOL_SCHEMAS` y un handler en + `TOOL_HANDLERS`; trata la entrada de las herramientas como no confiable. +- **Memoria**: interfaz `ConversationStore`, implementación con + diccionario, sustitúyela en `get_store()`. +- **Tests**: un cliente falso con guion, así que la suite funciona sin + conexión y es determinista. + +## Siguientes pasos + +- [Integración con MCP](mcp-integration.md) — si necesitas un servidor MCP + en lugar de un agente de chat. +- [Autenticación JWT](auth-jwt.md) — poner cuentas delante del endpoint de + chat. +- [¿Qué starter elegir?](../user-guide/choosing-a-starter.md) diff --git a/docs/es/tutorial/mcp-integration.md b/docs/es/tutorial/mcp-integration.md index 22bd259..34b2ba8 100644 --- a/docs/es/tutorial/mcp-integration.md +++ b/docs/es/tutorial/mcp-integration.md @@ -68,8 +68,8 @@ Deploying FastAPI project using 'fastapi-mcp' template │ Dependency 1 │ fastapi │ │ Dependency 2 │ uvicorn │ │ Dependency 3 │ pydantic │ -│ Dependency 4 │ python-jose │ -│ Dependency 5 │ passlib │ +│ Dependency 4 │ PyJWT │ +│ Dependency 5 │ pwdlib[argon2] │ │ Dependency 6 │ python-multipart│ │ Dependency 7 │ mcp │ └──────────────┴────────────────┘ @@ -131,21 +131,21 @@ ai-integrated-api/ ```python from datetime import datetime, timedelta from typing import Optional, Dict, Any -from jose import JWTError, jwt -from passlib.context import CryptContext +import jwt +from pwdlib import PasswordHash from src.core.config import settings -# Hash de contraseñas -pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") +# Hash de contraseñas — argon2id, el valor por defecto recomendado para aplicaciones nuevas. +password_hash = PasswordHash.recommended() def verify_password(plain_password: str, hashed_password: str) -> bool: """Verificación de contraseña""" - return pwd_context.verify(plain_password, hashed_password) + return password_hash.verify(plain_password, hashed_password) def get_password_hash(password: str) -> str: """Hash de contraseña""" - return pwd_context.hash(password) + return password_hash.hash(password) def create_access_token(data: Dict[str, Any], expires_delta: Optional[timedelta] = None) -> str: """Generación del access token""" @@ -189,7 +189,7 @@ def decode_token(token: str) -> Optional[Dict[str, Any]]: algorithms=[settings.ALGORITHM] ) return payload - except JWTError: + except jwt.PyJWTError: return None def verify_token(token: str, token_type: str = "access") -> Optional[str]: @@ -452,8 +452,7 @@ user_db = UserDatabase() from typing import Optional, List from fastapi import Depends, HTTPException, status, Security from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials, APIKeyHeader -from jose import JWTError - +import jwt from src.auth.jwt_handler import decode_token, token_manager from src.auth.models import User, UserInDB, Permission, user_db @@ -486,7 +485,7 @@ async def get_current_user( if user_id is None: raise credentials_exception - except JWTError: + except jwt.PyJWTError: raise credentials_exception user = user_db.get_user_by_id(user_id) diff --git a/docs/es/tutorial/sqlmodel.md b/docs/es/tutorial/sqlmodel.md new file mode 100644 index 0000000..ef32b77 --- /dev/null +++ b/docs/es/tutorial/sqlmodel.md @@ -0,0 +1,316 @@ +# Persistencia asíncrona con `fastapi-sqlmodel` + +Construye una API respaldada por una base de datos en la que cada concepto +se define **una sola vez**. Las clases de SQLModel son al mismo tiempo la +tabla de SQLAlchemy y el esquema pydantic, así que añadir una columna no +obliga a editar cuatro archivos. Este tutorial recorre la plantilla +`fastapi-sqlmodel`: la jerarquía de modelos, la base CRUD genérica, las +respuestas paginadas, las migraciones asíncronas de Alembic y cómo añadir tu +propio dominio. + +## Qué vas a aprender + +- Generar un proyecto con `fastkit startdemo fastapi-sqlmodel` +- Cómo `ItemBase` → `Item` / `ItemCreate` / `ItemUpdate` / `ItemPublic` + elimina la duplicación de esquemas +- Qué te da `CRUDBase` sin coste y dónde ampliarlo +- El envoltorio de listas `Page[T]` y dónde se aplican sus límites +- Ejecutar migraciones asíncronas de Alembic y pasar de SQLite a PostgreSQL + +## Requisitos previos + +- Python 3.12+ +- FastAPI-fastkit instalado (`pip install fastapi-fastkit`) +- Familiaridad con `async`/`await` y con los conceptos básicos de SQL + +Si prefieres el stack síncrono de PostgreSQL basado en Compose, consulta +[Integración con base de datos](database-integration.md), que usa +`fastapi-psql-orm`. + +## Paso 1: Generar y ejecutar + +```console +$ fastkit startdemo fastapi-sqlmodel +Enter the project name: catalog-api +Enter the author name: Developer Kim +Enter the author email: developer@example.com +Enter the project description: Product catalog service +Select package manager (pip, uv, pdm, poetry) [uv]: uv +Do you want to proceed with project creation? [y/N]: y +``` + +```console +$ cd catalog-api +$ bash scripts/migrate.sh # alembic upgrade head +$ bash scripts/run-server.sh # o: uvicorn src.app.main:app --reload +``` + +El `DATABASE_URL` por defecto es `sqlite+aiosqlite:///./app.db`, así que no +hay que instalar ningún servidor de base de datos. La documentación está en +. + +## Paso 2: El árbol generado + +``` +catalog-api/ +├── pyproject.toml # Metadatos PEP 621 + [tool.fastapi-fastkit] +├── requirements.txt +├── alembic.ini +├── Dockerfile +├── .env +├── scripts/ +│ ├── format.sh lint.sh migrate.sh run-server.sh test.sh +├── src/ +│ └── app/ +│ ├── main.py # app FastAPI + lifespan +│ ├── core/config.py # pydantic-settings +│ ├── db/ +│ │ ├── session.py # engine asíncrono, dependencia get_session +│ │ └── base.py # agregación de metadata para Alembic +│ ├── crud/ +│ │ ├── base.py # CRUDBase genérico +│ │ └── pagination.py # envoltorio Page[T] +│ ├── alembic/ +│ │ ├── env.py # entorno de migraciones asíncrono +│ │ └── versions/0001_create_items_table.py +│ ├── api/ +│ │ ├── router.py # agrega health + routers de dominio +│ │ └── health.py # GET /health +│ └── domains/items/ # el dominio de ejemplo +│ ├── models.py # tabla Item + esquemas Create/Update/Public +│ ├── crud.py # ItemCRUD(CRUDBase) +│ └── router.py # router de FastAPI +└── tests/ + ├── conftest.py test_health.py test_items.py test_crud_base.py +``` + +Fíjate en lo que *no* contiene una carpeta de dominio: no hay un +`schemas.py` separado de `models.py`, ni código repetitivo de repository por +dominio. Ambos desaparecen dentro de SQLModel y de `CRUDBase`, +respectivamente. + +## Paso 3: Una definición de modelo por concepto + +Abre `src/app/domains/items/models.py`. Todo deriva de una única base: + +```python +class ItemBase(SQLModel): + """Fields shared by the table and every request/response schema.""" + name: str = Field(min_length=1, max_length=128, index=True) + description: str | None = Field(default=None, max_length=512) + price: float = Field(ge=0) + in_stock: bool = True + + +class Item(ItemBase, table=True): # la tabla real + __tablename__ = "items" + id: int | None = Field(default=None, primary_key=True) + created_at: datetime = Field(default_factory=_utcnow) + + +class ItemCreate(ItemBase): ... # cuerpo de POST — sin campos asignados por el servidor + + +class ItemUpdate(SQLModel): # cuerpo de PATCH — todos los campos opcionales + name: str | None = Field(default=None, min_length=1, max_length=128) + ... + + +class ItemPublic(ItemBase): # respuesta — base + campos asignados por el servidor + id: int + created_at: datetime +``` + +Cuatro clases, cuatro funciones distintas, un único lugar donde viven los +campos: + +| Clase | Papel | Por qué está separada | +|--------------|------------------------------------|-----------------------| +| `ItemBase` | definiciones de campos compartidas | la única fuente de verdad | +| `Item` | la tabla de la base de datos | añade `id`, `created_at` | +| `ItemCreate` | cuerpo de la petición `POST` | no debe aceptar `id` | +| `ItemUpdate` | cuerpo de la petición `PATCH` | todos los campos opcionales | +| `ItemPublic` | cuerpo de la respuesta | expone los campos asignados por el servidor | + +**La recompensa:** añadir una columna `sku` significa añadir una línea a +`ItemBase`. La tabla obtiene la columna, el cuerpo de creación la acepta y +la respuesta la devuelve — sin ninguna otra edición. `ItemUpdate` se +mantiene explícito a propósito: la semántica de actualización parcial es una +decisión, no una derivación. + +## Paso 4: CRUD que no reescribes + +`src/app/crud/base.py` es una clase genérica parametrizada por el modelo de +tabla y sus dos esquemas de entrada: + +```python +class CRUDBase(Generic[ModelType, CreateSchemaType, UpdateSchemaType]): + async def get(self, session, obj_id) -> ModelType | None: ... + async def list(self, session, *, offset=0, limit=20) -> Sequence[ModelType]: ... + async def count(self, session) -> int: ... + async def create(self, session, obj_in) -> ModelType: ... + async def update(self, session, db_obj, obj_in) -> ModelType: ... + async def delete(self, session, db_obj) -> None: ... +``` + +Un detalle que conviene interiorizar — `update` usa `exclude_unset=True`: + +```python +changes: dict[str, Any] = obj_in.model_dump(exclude_unset=True) +``` + +Eso es lo que hace que `PATCH` sea realmente parcial. Un campo que el +cliente no envió no aparece en el diccionario, así que nunca se escribe; sin +`exclude_unset`, un `PATCH` sobrescribiría en silencio las columnas no +tocadas con los valores por defecto del esquema. + +Un dominio hereda de la base y añade solo lo que le es propio: + +```python +class ItemCRUD(CRUDBase[Item, ItemCreate, ItemUpdate]): + """CRUD for Item, extended with a name search.""" + + async def search_by_name(self, session, term, *, limit=20): + statement = select(Item).where(Item.name.like(f"%{term}%")).limit(limit) + result = await session.exec(statement) + return result.all() + + +item_crud = ItemCRUD(Item) +``` + +## Paso 5: Listas paginadas + +Los endpoints de listado devuelven un envoltorio `Page[T]` en lugar de un +array sin más: + +```json +{"items": [...], "total": 42, "offset": 0, "limit": 20} +``` + +El acotamiento se hace en el router, no en el cliente: + +```python +limit: int = Query(default=settings.DEFAULT_PAGE_LIMIT, ge=1) +... +limit = min(limit, settings.MAX_PAGE_LIMIT) +``` + +Así, `?limit=100000` no puede convertir una petición en un recorrido +completo de la tabla. `Page` también expone una propiedad `has_next` +derivada de `offset`, `len(items)` y `total`, que suele ser lo que de verdad +quiere un front-end. + +Los endpoints que trae el dominio items: + +| Método | Endpoint | Descripción | +|--------|---------------------------|--------------------------------------------| +| GET | `/api/v1/health` | Probe de liveness | +| GET | `/api/v1/items` | Listar items (`offset`, `limit`) | +| GET | `/api/v1/items/{item_id}` | Leer un item | +| POST | `/api/v1/items` | Crear un item (`201`) | +| PATCH | `/api/v1/items/{item_id}` | Actualizar parcialmente un item | +| DELETE | `/api/v1/items/{item_id}` | Eliminar un item (`204`) | + +Pruébalo: + +```console +$ curl -X POST http://127.0.0.1:8000/api/v1/items \ + -H 'Content-Type: application/json' \ + -d '{"name": "Keyboard", "price": 89.0}' + +$ curl 'http://127.0.0.1:8000/api/v1/items?offset=0&limit=5' +``` + +## Paso 6: Asíncrono de principio a fin + +`src/app/db/session.py` construye un `AsyncEngine` por proceso y entrega +sesiones con ámbito de petición: + +```python +async def get_session() -> AsyncGenerator[AsyncSession, None]: + async with async_session_factory() as session: + yield session +``` + +Los routers dependen de ella (`session: AsyncSession = Depends(get_session)`), +`CRUDBase` la recibe como primer argumento y cada llamada a la base de datos +se espera con `await`. Nada en el camino de la petición bloquea el event +loop por E/S. + +`create_db_and_tables()` también vive aquí y se llama desde el `lifespan` de +la app. Es una comodidad para el SQLite por defecto y para los tests — en +cuanto Alembic se encargue del esquema, define +`CREATE_TABLES_ON_STARTUP=false`. + +## Paso 7: Añadir un dominio + +1. Crea `src/app/domains//` imitando `items` + (`models.py`, `crud.py`, `router.py`). +2. **Importa el modelo de tabla en `src/app/db/base.py`.** Este es el paso + fácil de olvidar y confuso de depurar: el autogenerate de Alembic y + `create_db_and_tables` solo ven los modelos que se han importado, así que + una importación que falta aparece como una migración vacía en lugar de + como un error. +3. Registra el router en `src/app/api/router.py`. Las anclas + `# fastkit:imports` y `# fastkit:routes` marcan dónde inserta código + `fastkit addroute` — pon el tuyo en los mismos lugares. +4. Genera una migración: + `alembic revision --autogenerate -m "add "`. +5. Añade `tests/test_.py`, imitando `tests/test_items.py`. + +## Paso 8: Migraciones y PostgreSQL + +El entorno de Alembic en `src/app/alembic/env.py` es asíncrono, así que se +ejecuta sobre el mismo stack de drivers que la app. En el día a día: + +```console +$ bash scripts/migrate.sh # upgrade head +$ alembic revision --autogenerate -m "add sku to items" # tras un cambio en los modelos +``` + +Cambiar de base de datos es cambiar una URL más un driver: + +```console +$ uv pip install "asyncpg>=0.31.0" +$ export DATABASE_URL="postgresql+asyncpg://user:password@localhost:5432/app" +$ bash scripts/migrate.sh +``` + +No cambia nada en los modelos, el CRUD ni los routers — ese es el sentido de +mantener el driver detrás de `create_engine`. + +## Paso 9: Los tests + +```console +$ bash scripts/test.sh # o: pytest +``` + +Cada test se ejecuta contra una base de datos SQLite en memoria recién +creada: sin migraciones, sin archivos de fixtures, sin limpieza entre tests. +`test_crud_base.py` prueba directamente la base genérica — merece la pena +leerlo antes de ampliarla, porque documenta el comportamiento de +`exclude_unset` y el contrato de count/offset que heredan tus propios +dominios. + +## Resumen + +- **Generación**: `fastkit startdemo fastapi-sqlmodel` → + `bash scripts/migrate.sh` → `bash scripts/run-server.sh`. +- **Modelos**: `ItemBase` contiene los campos; la tabla y los esquemas de + E/S derivan de ella, así que una columna nueva se declara una sola vez. +- **CRUD**: `CRUDBase` proporciona get / list / count / create / update / + delete; los dominios heredan de ella para sus propias consultas. +- **Listas**: envoltorio `Page[T]` con acotamiento del límite en el servidor. +- **Esquema**: Alembic asíncrono; recuerda importar los modelos nuevos en + `src/app/db/base.py`. +- **Base de datos**: SQLite por defecto, PostgreSQL cambiando + `DATABASE_URL` e instalando `asyncpg`. + +## Siguientes pasos + +- [Autenticación JWT](auth-jwt.md) — pon cuentas y rutas protegidas delante + de esta capa de datos. +- [Proyecto orientado a dominios](domain-starter.md) — la estructura que + comparten ambas plantillas, sin base de datos. +- [¿Qué starter elegir?](../user-guide/choosing-a-starter.md) diff --git a/docs/es/user-guide/choosing-a-starter.md b/docs/es/user-guide/choosing-a-starter.md index bf02f9a..f4c502c 100644 --- a/docs/es/user-guide/choosing-a-starter.md +++ b/docs/es/user-guide/choosing-a-starter.md @@ -17,6 +17,9 @@ El resto de esta página explica por qué y cuándo elegir otra cosa. | Quieres el scaffold más pequeño posible | `fastkit init --interactive` (preset: **`minimal`**) | | Escribes un prototipo rápido / un script de un solo archivo | `fastkit init --interactive` (preset: **`single-module`**) | | Necesitas una base de datos real (PostgreSQL + SQLAlchemy + Alembic) | `fastkit startdemo fastapi-psql-orm` | +| Quieres persistencia asíncrona con migraciones y CRUD genérico | `fastkit startdemo fastapi-sqlmodel` | +| Necesitas cuentas de usuario, login y rutas protegidas desde el primer día | `fastkit startdemo fastapi-auth-jwt` | +| Construyes un backend de chat LLM con streaming y herramientas | `fastkit startdemo fastapi-llm-agent` | | Quieres un layout de dominio orientado a producción para una API mediana | `fastkit init --interactive` (preset: **`domain-starter`**) | ## `startdemo` vs `init --interactive` — ¿en qué se diferencian? @@ -112,11 +115,33 @@ Para ver el contrato completo, funcionalidad por funcionalidad (rutas de destino | `fastapi-empty` | `minimal` | Scaffold pelado; la misma forma a la que llega `minimal`. | | `fastapi-single-module` | `single-module` | Demo en un solo archivo. | | `fastapi-domain-starter` | `domain-starter` | Opción moderna recomendada; trae un dominio de ejemplo `items`. | -| `fastapi-async-crud` | `classic-layered` | Equivalente async a `fastapi-default`. | +| `fastapi-auth-jwt` | `domain-starter` | Tu API necesita cuentas. Access/refresh tokens, hash argon2id, roles y scopes, migraciones con Alembic. | +| `fastapi-sqlmodel` | `domain-starter` | Persistencia asíncrona ante todo: SQLModel + SQLAlchemy asíncrono, migraciones, una base CRUD genérica y listas paginadas. | +| `fastapi-llm-agent` | `domain-starter` | Un backend de chat Claude con streaming, un bucle real de llamadas a herramientas y tests sin conexión. | | `fastapi-custom-response` | `classic-layered` | Demuestra envelopes / formato de respuesta personalizados. | -| `fastapi-dockerized` | `classic-layered` | Añade un Dockerfile listo para producción al layout default. | -| `fastapi-psql-orm` | (sin preset directo) | PostgreSQL + SQLAlchemy + Alembic. Elígela cuando necesites una base de datos real. | +| `fastapi-psql-orm` | (sin preset directo) | PostgreSQL + SQLAlchemy + Alembic, síncrono. Elígela cuando quieras un stack de Compose alrededor de una base de datos real. | | `fastapi-mcp` | (sin preset directo) | Integración con Model Context Protocol. | +| `fastapi-async-crud` | `classic-layered` | ⚠️ **Obsoleta.** Equivalente async a `fastapi-default`; mejor usa `fastapi-sqlmodel`. | +| `fastapi-dockerized` | `classic-layered` | ⚠️ **Obsoleta.** Añade un Dockerfile al layout default; todas las plantillas actuales ya traen uno. | + +### Los tres starters nuevos + +Añadidos en v1.4.0, los tres siguen la forma de `domain-starter` (`src/app/` con carpetas por concepto), así que encajan con lo que ya conoces de ese preset. + +**`fastapi-auth-jwt` — autenticación que aguanta el contacto con producción.** Los access tokens duran poco; los refresh tokens rotan en cada uso y se registran por su `jti` en la base de datos, de modo que un token reutilizado se rechaza en lugar de aceptarse en silencio. Las contraseñas se hashean con argon2id mediante `pwdlib`. El logout es real: revoca una sesión o todas las sesiones de un usuario. Los guards vienen como dependencias — `get_current_user`, `get_current_active_superuser` y una fábrica `require_scopes(...)`. Funciona sobre SQLite sin servicios externos y pasa a PostgreSQL definiendo `DATABASE_URL`. Recorrido completo: [Autenticación JWT](../tutorial/auth-jwt.md). + +**`fastapi-sqlmodel` — una definición de modelo por concepto.** Una clase SQLModel es a la vez la tabla de SQLAlchemy y el esquema pydantic, así que una columna nueva se declara una vez y todos los esquemas de petición/respuesta la heredan. Asíncrono de principio a fin, migraciones asíncronas con Alembic, un `CRUDBase` que proporciona create / read / list / count / update / delete para que cada dominio solo escriba lo que le es propio, y endpoints de listado envueltos en un envoltorio de página. Recorrido completo: [Persistencia con SQLModel](../tutorial/sqlmodel.md). + +**`fastapi-llm-agent` — un backend de chat, no un envoltorio de `POST /completion`.** Los tokens llegan al cliente en streaming por SSE, y un bucle de llamadas a herramientas se ejecuta hasta que el modelo termina o salta el límite de iteraciones. El historial de conversación está detrás de una interfaz `ConversationStore`, así que pasar del diccionario incluido a Redis es cuestión de una implementación. La suite de tests sustituye el cliente de Anthropic por un objeto falso con guion — determinista, sin conexión, sin API key. Recorrido completo: [Agente LLM](../tutorial/llm-agent.md). + +### Sobre las plantillas obsoletas + +`fastapi-async-crud` y `fastapi-dockerized` se siguen distribuyendo y siguen generando proyectos que funcionan — no se rompe nada si las usas. Están marcadas como obsoletas porque el motivo para elegirlas ha desaparecido: + +- `fastapi-dockerized` existía para añadir un Dockerfile. Todas las plantillas actuales traen uno, y la selección de despliegue interactiva genera archivos de Docker para cualquier preset. +- `fastapi-async-crud` mostraba endpoints asíncronos sobre un almacén mock. `fastapi-sqlmodel` hace lo mismo contra una base de datos asíncrona real, con migraciones. + +Los proyectos existentes generados a partir de cualquiera de las dos no necesitan ninguna acción. `fastkit list-templates` muestra la lista viva con descripciones de una línea. @@ -141,5 +166,6 @@ La [matriz de presets de arquitectura](../reference/preset-feature-matrix.md) es - [Inicio rápido](quick-start.md) — crear de verdad tu primer proyecto. - [Crear proyectos](creating-projects.md) — recorrido más profundo de los flags de la CLI. +- [Tutorial de Autenticación JWT](../tutorial/auth-jwt.md), [tutorial de Persistencia con SQLModel](../tutorial/sqlmodel.md) y [tutorial de Agente LLM](../tutorial/llm-agent.md) — recorridos de principio a fin de los tres starters añadidos en v1.4.0. - [Tutorial del proyecto orientado a dominios](../tutorial/domain-starter.md) — si elegiste `domain-starter`, este es el recorrido de principio a fin del árbol generado, el ejemplo `items` incluido y cómo añadir tu siguiente dominio. - [Matriz de presets de arquitectura](../reference/preset-feature-matrix.md) — el contrato completo por preset y por feature. diff --git a/docs/es/user-guide/cli-reference.md b/docs/es/user-guide/cli-reference.md index ef10f6b..5ba0703 100644 --- a/docs/es/user-guide/cli-reference.md +++ b/docs/es/user-guide/cli-reference.md @@ -60,9 +60,161 @@ $ fastkit init [OPTIONS] | Opción | Descripción | Por defecto | |---|---|---| +| `--interactive` | Configuración guiada: preset de arquitectura y después selección de funcionalidades | off | +| `--config ` | Crea el proyecto a partir de un archivo de configuración guardado, sin prompts | - | +| `--save-config ` | Con `--interactive`, escribe la configuración respondida en esta ruta | - | +| `--project-name` | Nombre del proyecto (se pide si se omite) | - | +| `--author` | Nombre del autor (se pide si se omite) | - | +| `--author-email` | Email del autor (se pide si se omite) | - | +| `--description` | Descripción del proyecto (se pide si se omite) | - | | `--package-manager` | Gestor de paquetes a usar (pip, uv, pdm, poetry) | uv | +| `--dry-run` | Muestra los archivos y paquetes que se crearían, sin escribir nada | off | +| `--no-venv` | Omite la creación del entorno virtual (implica `--no-install`) | off | +| `--no-install` | Omite la instalación de dependencias | off | +| `--yes` / `-y` | No pide confirmación antes de sobrescribir archivos existentes al desplegar el proyecto in situ | off | | `--help` | Muestra la ayuda del comando | - | +#### Archivos de configuración (`--config` / `--save-config`) + +Las respuestas de una sesión interactiva son un simple mapeo, y ese mapeo +puede viajar en un archivo. Así la creación de un proyecto se convierte en +algo que puedes hacer commit, revisar y reproducir. + +```console +# grabar una sesión interactiva +$ fastkit init --interactive --save-config fastkit.config.json + +# reproducirla más tarde, sin ningún prompt +$ fastkit init --config fastkit.config.json +``` + +Si no pasas `--save-config`, una ejecución interactiva que llega al final +pregunta si quieres guardar las selecciones y dónde. Ese prompt solo aparece +cuando hay una persona delante del terminal — las ejecuciones con pipe o en +CI no preguntan nada y activan el guardado con el flag. + +**Los formatos soportados** se eligen según la extensión del archivo: + +| Extensión | Disponibilidad | +|---|---| +| `.json` | Siempre (biblioteca estándar) | +| `.toml` | Siempre (biblioteca estándar) | +| `.yaml` / `.yml` | Solo cuando PyYAML está instalado — fastkit no lo añade como dependencia de ejecución | + +Un archivo cargado debe contener un mapeo en su nivel superior con al menos +`project_name`, `author` y `author_email`. Después se valida con las mismas +reglas que usan los prompts interactivos (forma del nombre del proyecto, +formato del email, `all_dependencies` debe ser una lista), y se informa de +todos los problemas antes de escribir nada. Un archivo que solo enumera +selecciones de funcionalidades — sin `all_dependencies` — se expande a través +del builder interactivo, de modo que una configuración escrita a mano y una +sesión del asistente resuelven el mismo conjunto de paquetes. + +Un ejemplo mínimo: + +```json +{ + "project_name": "orders-api", + "author": "Developer Kim", + "author_email": "developer@example.com", + "description": "Domain-oriented orders service", + "architecture_preset": "domain-starter", + "package_manager": "uv" +} +``` + +#### Esquema del archivo de configuración + +Todo archivo de configuración — escrito a mano o producido por +`--save-config` — pasa por el mismo normalizador antes de llegar al generador +(`normalize_project_config` en +`backend/project_builder/config_schema.py`). Una clave desconocida, una +opción desconocida para un eje conocido o un valor de tipo incorrecto se +rechaza indicando el nombre problemático y la lista de valores realmente +permitidos, en lugar de ignorarse en silencio. + +| Clave | Forma | Notas | +|---|---|---| +| `project_name`, `author`, `author_email` | string | Obligatorias | +| `description` | string | Obligatoria | +| `architecture_preset` (alias `preset`) | `"minimal"` \| `"single-module"` \| `"classic-layered"` \| `"domain-starter"` | Si aparecen ambas formas, deben coincidir | +| `package_manager` | `"pip"` \| `"uv"` \| `"pdm"` \| `"poetry"` | | +| `database` | string, o `{"type": }` | `PostgreSQL`, `MySQL`, `MongoDB`, `Redis`, `SQLite`, `None` | +| `authentication`, `async_tasks`, `testing`, `caching`, `monitoring`, `migrations`, `logging` | string, o `{"type": }` | Ejes de selección única — consulta el [catálogo de funcionalidades](#catalogo-de-funcionalidades-del-builder-interactivo-interactive) más arriba para ver las opciones de cada eje | +| `utilities`, `tooling` | lista de strings | Ejes de selección múltiple — mismo catálogo para las opciones | +| `deployment` | lista de strings | Subconjunto de `Docker`, `docker-compose`; elegir `docker-compose` añade `Docker` automáticamente | +| `custom_packages` | lista de strings | Paquetes adicionales que se instalan tal cual | + +Cada eje acepta su opción sin más (`"authentication": "JWT"`) o la forma de +mapeo `{"type": ...}` que usan `database` y los propios metadatos +`[tool.fastapi-fastkit]` de un proyecto (`"authentication": {"type": "JWT"}`) +— ambas se normalizan a la misma forma canónica, y `--save-config` siempre +escribe de vuelta la forma canónica de string simple. + +Una configuración que usa la mayoría de los ejes: + +```json +{ + "project_name": "orders-api", + "author": "Developer Kim", + "author_email": "developer@example.com", + "description": "Domain-oriented orders service", + "architecture_preset": "domain-starter", + "package_manager": "uv", + "database": "PostgreSQL", + "authentication": "JWT", + "async_tasks": "Celery", + "caching": "Redis", + "migrations": "Alembic", + "tooling": ["ruff", "makefile"], + "deployment": ["Docker"] +} +``` + +La misma configuración en TOML: + +```toml +project_name = "orders-api" +author = "Developer Kim" +author_email = "developer@example.com" +description = "Domain-oriented orders service" +architecture_preset = "domain-starter" +package_manager = "uv" +database = "PostgreSQL" +authentication = "JWT" +async_tasks = "Celery" +caching = "Redis" +migrations = "Alembic" +tooling = ["ruff", "makefile"] +deployment = ["Docker"] +``` + +#### Previsualizar y omitir pasos + +`--dry-run` muestra el árbol que se crearía y los paquetes que se instalarían +— con el gestor de paquetes que los instalaría — y después termina sin tocar +el disco: + +```console +$ fastkit init --config fastkit.config.json --dry-run +``` + +`--no-install` se detiene después de crear el entorno virtual; +`--no-venv` omite también el entorno (y, por tanto, la instalación). +Ambos son útiles en contenedores, en CI y en cualquier lugar donde el +entorno se gestione fuera de fastkit. + +#### Despliegue in situ y confirmación de sobrescritura (`--yes` / `-y`) + +Cuando un proyecto se despliega in situ — en el workspace actual en lugar de +en una carpeta de proyecto nueva — fastkit pregunta "Overwrite these files?" +antes de sustituir cualquier archivo que ya exista. `--yes` (o su forma corta +`-y`) omite esa confirmación y procede con la sobrescritura. No afecta al +prompt independiente "Do you want to proceed with project creation?", que se +sigue mostrando. En un entorno no interactivo (stdin no es un TTY, como en CI +o en una ejecución con pipe), la confirmación de sobrescritura se omite +automáticamente incluso sin `--yes`. + #### Prompts interactivos El comando `init` te pedirá: @@ -96,6 +248,38 @@ El comando `init` te pedirá: - `redis` - Almacén de datos en memoria - `celery` - Cola distribuida de tareas +#### Catálogo de funcionalidades del builder interactivo (`--interactive`) + +`fastkit init --interactive` recorre un catálogo distinto, basado en ejes, en +lugar del stack minimal/standard/full anterior (fuente de verdad: +`FastkitConfig.PACKAGE_CATALOG` en `core/settings.py`, y +`DynamicConfigGenerator.build_artifacts()` para lo que genera cada opción). +Cada eje tiene `None` (omitir) como valor por defecto; `utilities` y +`tooling` son de selección múltiple. + +Ahora cada selección genera código real, no solo paquetes instalados — +consulta la [matriz de presets de arquitectura / funcionalidades](../reference/preset-feature-matrix.md) +para ver en qué se diferencian `minimal` / `single-module` (`main.py` +regenerado) y `classic-layered` / `domain-starter` (`main.py` conservado, +avisos de cableado manual). + +| Eje | Opciones | Paquetes instalados | Archivos generados | +|---|---|---|---| +| `database` | PostgreSQL, MySQL, MongoDB, Redis, SQLite, None | PostgreSQL: `asyncpg`, `sqlalchemy` · MySQL: `aiomysql`, `sqlalchemy` · MongoDB: `motor` · Redis: `redis[hiredis]` · SQLite: `sqlalchemy`, `aiosqlite` | Un módulo de configuración de base de datos en la ruta del preset (p. ej. `src/config/database.py`) | +| `authentication` | JWT, OAuth2, FastAPI-Users, Session-based, None | JWT: `pyjwt[crypto]`, `pwdlib[argon2]` · OAuth2: `authlib`, `itsdangerous`, `httpx` · FastAPI-Users: `fastapi-users[sqlalchemy]`, `pyjwt[crypto]`, `pwdlib[argon2]` · Session-based: `itsdangerous` | Un módulo de configuración de auth en la ruta del preset; OAuth2 y Session-based añaden además la configuración de middleware en `main.py` | +| `async_tasks` | Celery, Dramatiq, None | Celery: `celery[redis]`, `redis[hiredis]` · Dramatiq: `dramatiq[redis]`, `redis[hiredis]` | `/worker.py` (worker en segundo plano) + `/features/tasks.py` (rutas de tareas) | +| `testing` | Basic, Coverage, Advanced, None | Basic: `pytest`, `pytest-asyncio`, `httpx` · Coverage: + `pytest-cov` · Advanced: + `faker`, `factory-boy` | `pytest.ini`; Advanced añade además `tests/factories.py` y `tests/test_factories.py` | +| `caching` | Redis, None | Redis: `redis[hiredis]`, `fastapi-cache2`, `jinja2` | `/features/cache.py` (endpoints con caché) | +| `monitoring` | Loguru, OpenTelemetry, Prometheus, None | Loguru: `loguru` · OpenTelemetry: `opentelemetry-api`, `opentelemetry-sdk`, `opentelemetry-instrumentation-fastapi`, `opentelemetry-exporter-otlp-proto-http` · Prometheus: `prometheus-client`, `prometheus-fastapi-instrumentator` | Sin archivo independiente — se cablea directamente en `main.py` (import / lifespan / setup) | +| `utilities` (selección múltiple) | CORS, Rate-Limiting, Pagination, WebSocket, None | CORS: ninguno (integrado en FastAPI) · Rate-Limiting: `slowapi` · Pagination: `fastapi-pagination` · WebSocket: `websockets` | CORS y Rate-Limiting se cablean directamente en `main.py`; Pagination añade `/features/pagination.py` y WebSocket añade `/features/websocket.py` | +| `migrations` | Alembic, None | Alembic: `alembic` | `alembic.ini`, `alembic/env.py`, `alembic/script.py.mako`, una revisión base `alembic/versions/_initial.py`, `scripts/migrate.sh` — solo cuando el eje de base de datos es una base de datos SQL | +| `tooling` (selección múltiple) | ruff, pre-commit, github-actions, devcontainer, makefile, None | ruff: `ruff` · pre-commit: `pre-commit` · github-actions / devcontainer / makefile: ninguno | ruff fusiona un bloque `[tool.ruff]` en `pyproject.toml` (o escribe `ruff.toml` en proyectos `pip`); pre-commit escribe `.pre-commit-config.yaml`; github-actions escribe `.github/workflows/test.yml`; devcontainer escribe `.devcontainer/devcontainer.json`; makefile escribe `Makefile` | +| `logging` | structured, None | Ninguno (solo `logging` + `json` de la stdlib) | `/logging_config.py` (logging JSON estructurado + middleware de request-id), cableado en `main.py` | + +Sea cual sea la selección, cada proyecto generado obtiene los endpoints +`/health` y `/ready` en `main.py`, y `--dry-run` muestra exactamente este +conjunto de archivos antes de escribir nada. + #### Ejemplos
@@ -227,6 +411,35 @@ Crea endpoints CRUD completos: | `PUT` | `/api/v1/users/{user_id}` | Actualizar un usuario | | `DELETE` | `/api/v1/users/{user_id}` | Eliminar un usuario | +#### Dónde se inserta el código + +`addroute` necesita dos cosas del proyecto: dónde poner los nuevos imports y +dónde registrar el router. Las plantillas y los entrypoints generados marcan +ambos lugares con comentarios ancla: + +```python +# src/app/api/router.py +from src.app.api import health + +# fastkit:imports + +api_router = APIRouter() +api_router.include_router(health.router) + +# fastkit:routes +``` + +Mantén esos comentarios en su sitio cuando edites el archivo — `addroute` +inserta justo encima de `# fastkit:imports` y `# fastkit:routes`. Un +proyecto que los haya perdido (o que se generó antes de que existieran las +anclas) sigue funcionando: fastkit recurre a una inserción basada en AST que +encuentra por sí misma el bloque de imports y los registros de routers. Las +anclas simplemente hacen que el resultado sea predecible. + +`addroute` también lee el bloque `[tool.fastapi-fastkit]` del proyecto para +localizar el entrypoint, así que funciona en estructuras que el escaneo del +disco por sí solo clasificaría mal. + ### `startdemo` Crea un proyecto FastAPI a partir de una plantilla ya preparada. @@ -241,9 +454,24 @@ $ fastkit startdemo [OPTIONS] | Opción | Descripción | Por defecto | |---|---|---| +| `--project-name` | Nombre del proyecto (se pide si se omite) | - | +| `--author` | Nombre del autor (se pide si se omite) | - | +| `--author-email` | Email del autor (se pide si se omite) | - | +| `--description` | Descripción del proyecto (se pide si se omite) | - | | `--package-manager` | Gestor de paquetes a usar (pip, uv, pdm, poetry) | uv | +| `--dry-run` | Muestra los archivos y paquetes que se crearían, sin escribir nada | off | +| `--no-venv` | Omite la creación del entorno virtual (implica `--no-install`) | off | +| `--no-install` | Omite la instalación de dependencias | off | +| `--yes` / `-y` | No pide confirmación antes de sobrescribir archivos existentes al desplegar el proyecto in situ | off | | `--help` | Muestra la ayuda del comando | - | +Igual que `init`, `startdemo` se niega a ejecutarse cuando el directorio del +proyecto de destino ya existe (`Error: Project '{name}' already exists.`). +`--dry-run` es la excepción: como nunca escribe en disco, sí puede apuntar a +un nombre que ya tiene directorio. Consulta +[Despliegue in situ y confirmación de sobrescritura](#despliegue-in-situ-y-confirmacion-de-sobrescritura-yes-y) +más arriba para ver qué hace `--yes` al desplegar en el workspace actual. + #### Prompts interactivos El comando `startdemo` te pedirá: @@ -259,11 +487,21 @@ El comando `startdemo` te pedirá: | Plantilla | Descripción | Funcionalidades | |---|---|---| | `fastapi-default` | Proyecto FastAPI simple | CRUD básico, datos mock | -| `fastapi-async-crud` | API de gestión de items async | async/await, rendimiento | +| `fastapi-domain-starter` | Starter orientado a dominios | Una carpeta por concepto de negocio, `/health` | +| `fastapi-auth-jwt` | Autenticación JWT | Rotación de access/refresh, argon2id, roles y scopes, Alembic | +| `fastapi-sqlmodel` | Persistencia asíncrona con SQLModel | SQLModel + SQLAlchemy asíncrono, Alembic, CRUD genérico, paginación | +| `fastapi-llm-agent` | Agente Claude con streaming | Streaming SSE, bucle de llamadas a herramientas, memoria de conversación | | `fastapi-custom-response` | Sistema de respuesta personalizada | Respuestas a medida, paginación | -| `fastapi-dockerized` | API FastAPI dockerizada | Docker, listo para producción | | `fastapi-psql-orm` | API FastAPI con PostgreSQL | PostgreSQL, SQLAlchemy, Alembic | +| `fastapi-mcp` | Servidor Model Context Protocol | Integración con MCP | +| `fastapi-single-module` | Sandbox de un solo archivo | Un módulo, sin fronteras de paquete | | `fastapi-empty` | Proyecto FastAPI mínimo | Configuración mínima | +| `fastapi-async-crud` | *(obsoleta)* API de gestión de items async | Sustituida por `fastapi-sqlmodel` | +| `fastapi-dockerized` | *(obsoleta)* API FastAPI dockerizada | Las herramientas de Docker vienen ahora con las plantillas más nuevas | + +Las plantillas obsoletas siguen generando proyectos que funcionan; +simplemente ya no son puntos de partida recomendados. Consulta +[¿Qué starter elegir?](choosing-a-starter.md). #### Ejemplos @@ -331,9 +569,14 @@ INFO: Uvicorn running on http://127.0.0.1:8000 #### Requisitos - Debe ejecutarse desde el directorio de un proyecto FastAPI -- El proyecto debe tener `src/main.py` con la app FastAPI +- El proyecto debe exponer una app FastAPI — se resuelve desde + `[tool.fastapi-fastkit].app_module` cuando existe y, si no, se descubre + buscando `main.py` - El entorno virtual debe estar activado +Si ni el entrypoint registrado ni el escaneo encuentran un módulo, +`runserver` informa de que no ha podido encontrar `main.py` y se detiene. + ### `list-templates` Lista todas las plantillas de proyecto FastAPI disponibles. @@ -372,6 +615,43 @@ $ fastkit list-templates
+## Metadatos del proyecto (`[tool.fastapi-fastkit]`) + +Cada proyecto que genera fastkit registra cómo se produjo, en su propio +`pyproject.toml`: + +```toml +[tool.fastapi-fastkit] +managed = true +version = "1.4.0" +template = "fastapi-domain-starter" +preset = "domain-starter" +package_manager = "uv" +app_module = "src.app.main:app" +features = ["database:PostgreSQL", "authentication:JWT"] +``` + +| Clave | Significado | +|---|---| +| `managed` | Siempre `true`; marca el proyecto como gestionado por fastkit | +| `version` | La versión de fastkit que generó el proyecto | +| `template` | La plantilla de `startdemo`, o la plantilla base del preset | +| `preset` | El preset de arquitectura de `init --interactive` (se omite en otro caso) | +| `package_manager` | El gestor usado para construir el entorno | +| `app_module` | El entrypoint `module:attr` de uvicorn | +| `features` | Lista plana `":"` de las selecciones interactivas | + +Este bloque es la única fuente de verdad del entrypoint del proyecto: +`fastkit runserver` lee `app_module` de aquí antes de recurrir al escaneo del +árbol, `fastkit addroute` lo usa para localizar la aplicación y los +Dockerfiles generados usan el mismo valor en su `CMD`. + +El bloque se escribe al final de la generación — los gestores de paquetes +reescriben `pyproject.toml` mientras resuelven dependencias, así que hay que +estamparlo después — y una regeneración posterior lo sustituye en lugar de +añadir una segunda copia. Editar `app_module` a mano es la forma soportada de +indicar a fastkit una estructura que has reorganizado tú mismo. + ## Variables de entorno FastAPI-fastkit respeta estas variables de entorno: @@ -381,6 +661,19 @@ FastAPI-fastkit respeta estas variables de entorno: | `FASTKIT_CONFIG_DIR` | Directorio de configuración | `~/.fastkit` | | `FASTKIT_TEMPLATES_DIR` | Directorio de plantillas personalizadas | Plantillas integradas | | `FASTKIT_LOG_LEVEL` | Nivel de log | `INFO` | +| `FASTKIT_SUBPROCESS_TIMEOUT` | Sustituye todos los timeouts de los subprocesos del gestor de paquetes, en segundos | por operación (30 / 120 / 900) | + +Cada llamada al gestor de paquetes tiene su propio límite, para que un +proceso hijo colgado nunca pueda bloquear la CLI: 30 segundos para una +comprobación de disponibilidad, 120 para crear un entorno virtual y 900 para +instalar dependencias. `FASTKIT_SUBPROCESS_TIMEOUT` sustituye los tres por un +único valor — útil en una red lenta o para fallar rápido en CI. Un valor que +no sea un entero positivo se ignora. + +```console +$ export FASTKIT_SUBPROCESS_TIMEOUT=1800 +$ fastkit startdemo fastapi-sqlmodel +``` ### Ejemplos diff --git a/docs/es/user-guide/creating-projects.md b/docs/es/user-guide/creating-projects.md index 7541723..b5830b8 100644 --- a/docs/es/user-guide/creating-projects.md +++ b/docs/es/user-guide/creating-projects.md @@ -462,7 +462,7 @@ Tras crear el proyecto, puedes añadir más dependencias:
```console -$ pip install requests httpx python-jose +$ pip install requests httpx pyjwt $ pip freeze > requirements.txt ``` diff --git a/docs/fr/contributing/development-setup.md b/docs/fr/contributing/development-setup.md index 52f92b8..6e8e725 100644 --- a/docs/fr/contributing/development-setup.md +++ b/docs/fr/contributing/development-setup.md @@ -212,6 +212,27 @@ Le Makefile du projet fournit des commandes pratiques pour les tâches courantes | `make inspect-templates-verbose` | Lancer l'inspection avec une sortie détaillée | | `make inspect-template` | Inspecter un ou plusieurs modèles précis (paramètre TEMPLATES) | +Le script sous-jacent accepte trois options utiles à connaître pendant les itérations : `--offline` (ignore les vérifications de fraîcheur sur PyPI), `--no-smoke` (ignore le démarrage du projet généré avec uvicorn — l'étape la plus lente) et `--mypy` (vérifie les types du projet généré ; option à activer explicitement car elle est lente). Consultez [Assurance qualité des modèles](../reference/template-quality-assurance.md) pour savoir ce que fait réellement chaque vérification. + +```console +$ python scripts/inspect-templates.py --templates fastapi-sqlmodel --offline --no-smoke +``` + +### Le code généré vit dans des fragments Jinja2 + +Tout ce que `fastkit init --interactive` écrit dans un projet — `main.py`, modules de base de données et d'authentification, fichiers Docker, configuration pytest — est rendu à partir de modèles situés sous `src/fastapi_fastkit/fragments/`, et non assemblé à partir de chaînes en Python. `main.py.j2` ne code en dur aucune fonctionnalité : il parcourt les listes `import_fragments`, `lifespan_fragments` et `setup_fragments` fournies par `DynamicConfigGenerator` et inclut chacun de leurs éléments, ce qui permet de combiner librement les fonctionnalités. + +Ajouter une fonctionnalité consiste grosso modo à : + +1. Déposer les morceaux dans `main/imports/`, `main/lifespan/` et/ou `main/setup/` (les fragments lifespan sont indentés de quatre espaces ; les deux autres ne le sont pas). +2. Les enregistrer dans `DynamicConfigGenerator._main_fragments()`, conditionnés par la recherche de configuration correspondante. +3. Pour un module autonome, ajouter `/.py.j2` et le rendre avec `self._render(...)`. +4. Étendre la matrice de combinaisons dans `tests/test_backends/test_project_builder_config_generator.py` — elle vérifie que chaque fichier Python rendu est analysable avec `ast.parse`. + +Deux contraintes sur lesquelles on trébuche facilement : chaque fragment doit se terminer par un saut de ligne final (l'environnement est construit avec `keep_trailing_newline=True`), et `main.py.j2` doit conserver ses ancres `# fastkit:imports` / `# fastkit:routes` — `fastkit addroute` insère du code à ces emplacements. + +La procédure de référence se trouve à côté des fragments eux-mêmes, dans [`src/fastapi_fastkit/fragments/README.md`](https://github.com/bnbong/FastAPI-fastkit/blob/main/src/fastapi_fastkit/fragments/README.md). + ### Commandes de documentation | Commande | Description | @@ -353,13 +374,16 @@ FastAPI-fastkit/ - **`src/fastapi_fastkit/`** — code source du paquet principal - **`cli.py`** — point d'entrée principal du CLI - **`backend/`** — logique backend centrale - - **`inspector.py`** — inspecteur de modèles + - **`inspection/`** — pipeline d'inspection des modèles (contexte, vérifications, cohérence, fraîcheur, smoke, lint, rapport) + - **`inspector.py`** — façade au-dessus de `inspection/`, conservée pour le chemin d'import historique - **`interactive/`** — composants du mode interactif (invites, sélecteurs, validateurs) - **`package_managers/`** — implémentations des gestionnaires de paquets (pip, uv, pdm, poetry) - - **`project_builder/`** — utilitaires de construction de projet + - **`project_builder/`** — utilitaires de construction de projet, dont le générateur de configuration piloté par les fragments + - **`scaffolder.py`** — le pipeline de génération partagé par `init` et `startdemo` - **`transducer.py`** — transducer de modèle - **`core/`** — configuration centrale et exceptions - - **`fastapi_project_template/`** — modèles de projet (fastapi-default, fastapi-async-crud, etc.) + - **`fragments/`** — fragments Jinja2 rendus dans les projets générés (voir ci-dessus) + - **`fastapi_project_template/`** — modèles de projet (fastapi-default, fastapi-domain-starter, etc.) - **`utils/`** — fonctions utilitaires partagées - **`tests/`** — suite de tests - **`test_backends/`** — tests propres au backend diff --git a/docs/fr/contributing/template-creation-guide.md b/docs/fr/contributing/template-creation-guide.md index 61932ee..53cf65d 100644 --- a/docs/fr/contributing/template-creation-guide.md +++ b/docs/fr/contributing/template-creation-guide.md @@ -97,8 +97,7 @@ fastapi-{template-name}/ │ ├── lint.sh-tpl # Vérifications de style │ ├── run-server.sh-tpl # Lancement du serveur │ └── test.sh-tpl # Exécution des tests -├── pyproject.toml-tpl # ✅ Métadonnées principales (PEP 621, recommandé) -├── setup.py-tpl # 🟡 Métadonnées legacy (acceptées pour la rétrocompatibilité) +├── pyproject.toml-tpl # ✅ Métadonnées principales (PEP 621, requis) ├── requirements.txt-tpl # 🟡 Optionnel si pyproject déclare déjà les dépendances ├── setup.cfg-tpl # Configuration des outils de développement ├── README.md-tpl # ✅ Documentation du projet (obligatoire) @@ -110,10 +109,16 @@ fastapi-{template-name}/ - un répertoire `tests/` - un fichier `README.md-tpl` -- au moins un fichier de métadonnées : `pyproject.toml-tpl` (préféré, PEP 621) ou `setup.py-tpl` (legacy, toujours accepté) -- une déclaration de `fastapi` comme dépendance dans au moins l'un de : `pyproject.toml-tpl` `[project].dependencies`, `requirements.txt-tpl`, ou `setup.py-tpl` `install_requires` +- `pyproject.toml-tpl` (PEP 621) +- une déclaration de `fastapi` comme dépendance dans `pyproject.toml-tpl` `[project].dependencies` ou dans `requirements.txt-tpl` -`requirements.txt-tpl` n'est plus strictement requis quand `pyproject.toml-tpl` déclare `[project].dependencies`. Les modèles modernes DEVRAIENT adopter `pyproject.toml-tpl` comme fichier de métadonnées principal. +`requirements.txt-tpl` n'est pas requis quand `pyproject.toml-tpl` déclare `[project].dependencies` — mais lorsqu'un modèle fournit les deux, ils doivent concorder : l'inspection échoue en cas de divergence des dépendances entre eux. + +**Les nouveaux modèles sont pyproject-first et ne doivent pas fournir de `setup.py-tpl`.** Aucun modèle livré n'en contient depuis v1.4.0. L'inspection accepte toujours ce fichier pour que les modèles hors dépôt continuent de fonctionner, mais il s'agit d'un chemin legacy. + +**Ciblez Python 3.12 de manière cohérente** : `requires-python = ">=3.12"`, black `target-version = ["py312"]`, mypy `python_version = "3.12"`, et `python:3.12-slim` pour toute image Docker de base. L'inspection vérifie que ces trois épinglages concordent. + +**Ne déclarez jamais `FastAPI-fastkit` comme dépendance d'exécution** d'un projet généré. Le CLI est un outil de développement ; la liste des dépendances d'un modèle ne doit contenir que ce que l'application générée importe elle-même. ### Guide d'écriture des fichiers @@ -237,52 +242,23 @@ isort==5.12.0 mypy==1.7.1 ``` -#### 4. Écrire setup.py-tpl (legacy — optionnel quand pyproject est présent) +#### 4. Ancres de routes -Conservé pour les modèles legacy. Les nouveaux modèles n'ont pas besoin de ce fichier s'ils embarquent `pyproject.toml-tpl`. +Les modèles qui fournissent un `main.py-tpl` ou un module d'agrégation d'API doivent marquer l'endroit où `fastkit addroute` insère du code : ```python -""" - package setup +from src.app.api import health -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", -] +# fastkit:imports -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", - ], -) +api_router = APIRouter() +api_router.include_router(health.router) + +# fastkit:routes ``` +Sans ces ancres, `addroute` se rabat sur une insertion fondée sur l'AST qui repère elle-même le bloc d'imports et les enregistrements de routeurs — un modèle qui en est dépourvu fonctionne donc toujours, mais le résultat est moins prévisible. Conservez ces commentaires lorsque vous modifiez ces fichiers. + #### 5. Écrire les fichiers de test ```python @@ -342,8 +318,17 @@ make inspect-template TEMPLATES="fastapi-your-template" # Validate with verbose output python scripts/inspect-templates.py --templates "fastapi-your-template" --verbose + +# Fast local loop: no network, no server boot +python scripts/inspect-templates.py --templates "fastapi-your-template" \ + --offline --no-smoke + +# Full check before opening a PR (adds mypy over the generated project) +python scripts/inspect-templates.py --templates "fastapi-your-template" --mypy ``` +`--offline` ignore les vérifications de fraîcheur des dépendances sur PyPI, `--no-smoke` ignore le démarrage du projet généré (l'étape la plus lente) et `--mypy` vérifie les types du projet généré. Consultez [Assurance qualité des modèles](../reference/template-quality-assurance.md) pour savoir ce que fait chaque vérification. + !!! note Lorsque vous soumettez une PR, le workflow **Template PR Inspection** se déclenche automatiquement et valide vos changements de modèle. Vous recevrez le retour directement sur votre PR. @@ -356,7 +341,7 @@ L'inspecteur valide automatiquement les éléments suivants : - [ ] Le répertoire `tests/` existe - [ ] Le fichier `README.md-tpl` existe -- [ ] Au moins l'un de `pyproject.toml-tpl` (préféré) ou `setup.py-tpl` (legacy) existe +- [ ] `pyproject.toml-tpl` existe (pas de `setup.py-tpl` dans les nouveaux modèles) #### ✅ Validation des extensions de fichiers @@ -368,7 +353,8 @@ L'inspecteur valide automatiquement les éléments suivants : - [ ] `fastapi` est déclaré dans au moins l'un de : - [ ] `pyproject.toml-tpl` sous `[project].dependencies` (préféré) - [ ] `requirements.txt-tpl` - - [ ] `setup.py-tpl` sous `install_requires` +- [ ] `requirements.txt-tpl` et `[project].dependencies` ne divergent pas +- [ ] `FastAPI-fastkit` n'est **pas** une dépendance d'exécution #### ✅ Validation de l'implémentation FastAPI @@ -381,6 +367,24 @@ L'inspecteur valide automatiquement les éléments suivants : - [ ] L'installation des dépendances réussit - [ ] Tous les tests pytest passent +#### ✅ Test smoke + +- [ ] Le projet généré démarre sous uvicorn +- [ ] `/docs` répond +- [ ] `/health` répond + +La cible uvicorn est résolue à partir de `[tool.fastapi-fastkit].app_module` dans le `pyproject.toml` généré, puis de `app_module` dans `template-config.yml`, puis des emplacements conventionnels de `main.py`. + +#### ✅ Cohérence de la configuration + +- [ ] `requires-python`, black `target-version` et mypy `python_version` indiquent tous Python 3.12 +- [ ] Aucune divergence de dépendances entre `requirements.txt-tpl` et `[project].dependencies` +- [ ] Aucune auto-dépendance à `FastAPI-fastkit` + +#### ✅ Résidus d'emplacements + +- [ ] Aucun `` ne subsiste après la génération du projet + #### ✅ Tests automatisés des modèles FastAPI-fastkit inclut des **tests automatisés des modèles** qui exécutent des tests complets pour tous les modèles : diff --git a/docs/fr/index.md b/docs/fr/index.md index b923d26..83c5892 100644 --- a/docs/fr/index.md +++ b/docs/fr/index.md @@ -34,6 +34,8 @@ Ce projet s'inspire de l'`initializer` de SpringBoot et de l'opération CLI `dja - **🔍 Assurance qualité automatisée des modèles** : des tests automatisés hebdomadaires garantissent que tous les modèles restent fonctionnels et à jour - **🚀 Plusieurs modèles de projet** : choisissez parmi divers modèles préconfigurés pour différents cas d'usage (async CRUD, Docker, PostgreSQL, etc.) - **📦 Prise en charge de plusieurs gestionnaires de paquets** : choisissez votre gestionnaire de paquets Python préféré (pip, uv, pdm, poetry) pour la gestion des dépendances +- **🧾 Configurations de projet reproductibles** : enregistrez une session interactive avec `--save-config` et rejouez-la plus tard avec `fastkit init --config` — aucune question, le même projet +- **🏷️ Projets auto-descriptifs** : chaque projet généré enregistre son modèle, son préréglage, son gestionnaire de paquets, son point d'entrée et les fonctionnalités sélectionnées dans un bloc `[tool.fastapi-fastkit]` que `runserver` et `addroute` relisent ## Installation @@ -123,7 +125,7 @@ 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 │ +│ ℹ Injected metadata into pyproject.toml │ ╰──────────────────────────────────────────────────────╯ ╭──────────────────────── Info ────────────────────────╮ │ ℹ Injected metadata into config file │ @@ -174,6 +176,16 @@ Installing dependencies... Cette commande crée un nouvel espace de travail FastAPI avec son environnement virtuel Python. +- Options principales : + - `--project-name`, `--author`, `--author-email`, `--description` + - `--package-manager` [pip|uv|pdm|poetry] + - `--config ` : crée le projet à partir d'un fichier de configuration enregistré (`.json` / `.toml`, et `.yaml` lorsque PyYAML est installé), sans aucune question + - `--save-config ` : avec `--interactive`, enregistre les réponses pour une réutilisation ultérieure + - `--dry-run` : affiche les fichiers et paquets qui seraient créés, sans rien écrire + - `--no-venv` : ignore la création de l'environnement virtuel (implique `--no-install`) + - `--no-install` : ignore l'installation des dépendances + - `--yes` / `-y` : ignore la confirmation « Overwrite these files? » lors d'un déploiement sur place + ### Créer un projet en mode interactif ✨ NOUVEAU ! Pour les projets plus complexes, utilisez le **mode interactif** afin de construire votre application FastAPI étape par étape, avec une sélection intelligente des fonctionnalités : @@ -264,6 +276,30 @@ Select utilities (comma-separated numbers, e.g., 1,3,4): Select utilities: 1 +🪵 Logging Format +Select logging format: + 1. structured - JSON logs + X-Request-ID correlation middleware (stdlib only) + 2. None - Uvicorn's default plain-text logging + +Select logging format [2]: 1 + +🧬 Database Migrations +Select migration tool: + 1. Alembic - alembic.ini + async alembic/env.py + scripts/migrate.sh (recommended for SQL databases) + 2. None - No migration tooling + +Select migration tool [1]: 1 + +🧰 Developer Tooling +Select tooling to generate (comma-separated numbers, e.g., 1,3) + 1. ruff ruff + ruff-format config in pyproject.toml (replaces black/isort) + 2. pre-commit .pre-commit-config.yaml + 3. github-actions .github/workflows/test.yml (Python 3.12) + 4. devcontainer .devcontainer/devcontainer.json + 5. makefile Makefile with install/test/lint/format/run + +Your choice (or press Enter to skip): 1,2 + 🚀 Deployment Configuration Select deployment option: 1. Docker - Generate Dockerfile @@ -294,6 +330,9 @@ Enter custom package names (comma-separated, press Enter to skip): │ Monitoring │ Prometheus │ │ Testing │ Coverage │ │ Utilities │ CORS │ +│ Logging │ structured │ +│ Migrations │ Alembic │ +│ Tooling │ ruff, pre-commit │ │ Package Manager │ uv │ └─────────────────────┴───────────────────────────────────────────────────────────────────────────┘ @@ -350,6 +389,16 @@ Le mode interactif offre : - **Gestion intelligente des dépendances** avec compatibilité automatique pour pip - **Validation des fonctionnalités** avec des avertissements de câblage manuel pour les sélections que le préréglage ne peut pas câbler automatiquement - **Marqueurs d'identité** dans le `pyproject.toml` généré (marqueur de description + table `[tool.fastapi-fastkit]`) afin que `is_fastkit_project()` puisse reconnaître les projets générés par la suite +- **Métadonnées du projet** enregistrées dans `[tool.fastapi-fastkit]` — modèle, préréglage, gestionnaire de paquets, `app_module` et les `features` sélectionnées — que `fastkit runserver` et `fastkit addroute` relisent +- **Exécutions reproductibles** : `--save-config ` enregistre les réponses, `--config ` les rejoue sans la moindre question, et `--dry-run` prévisualise le résultat sans rien écrire + +Chaque sélection du catalogue génère désormais du code réel et fonctionnel au lieu de se contenter d'installer un paquet — tâches en arrière-plan (Celery/Dramatiq), mise en cache Redis, WebSocket, pagination, traçage OpenTelemetry et authentification OAuth2 / par session sont toutes fournies avec un module correspondant et leur câblage dans `main.py`. Trois nouveaux axes complètent le catalogue : + +- **Journalisation** : `structured` ajoute une sortie structurée basée sur `logging` + `json` de la bibliothèque standard, avec un middleware d'identifiant de requête — aucune nouvelle dépendance +- **Migrations** : `Alembic` génère un environnement de migration asynchrone complet (`alembic.ini`, `alembic/env.py`, une révision de référence, `scripts/migrate.sh`) pour toute sélection de base de données SQL +- **Outillage** (sélection multiple) : `ruff`, `pre-commit`, `github-actions`, `devcontainer`, `makefile` — chacun génère son propre fichier de configuration ; `ruff` fusionne aussi un bloc `[tool.ruff]` dans `pyproject.toml` + +Chaque projet généré reçoit également les points d'extrémité `/health` et `/ready`, et `--dry-run` liste exactement le même ensemble de fichiers qu'une exécution réelle écrirait, y compris ces nouveaux artefacts. `classic-layered` et `domain-starter` conservent le `main.py` qu'ils fournissent : la validation des fonctionnalités avertit donc lorsqu'une sélection sur l'un de ces axes nécessite un câblage manuel — consultez la [matrice préréglages / fonctionnalités](reference/preset-feature-matrix.md) pour plus de détails. ### Ajouter une nouvelle route au projet FastAPI @@ -445,6 +494,12 @@ FastAPI template project will deploy at '~your-project-path~'
+- Options principales : + - `--project-name`, `--author`, `--author-email`, `--description` + - `--package-manager` [pip|uv|pdm|poetry] + - `--dry-run`, `--no-venv`, `--no-install`, `--yes`/`-y` — même signification que pour `init` + - Refuse de s'exécuter si le répertoire du projet cible existe déjà (sauf avec `--dry-run`) + Pour consulter la liste des démos FastAPI disponibles, vérifiez avec :
@@ -463,18 +518,42 @@ $ fastkit list-templates │ │ PostgreSQL │ │ fastapi-default │ Simple FastAPI Project │ │ fastapi-single-module │ FastAPI Single Module Template │ +│ fastapi-auth-jwt │ FastAPI JWT Authentication │ +│ fastapi-sqlmodel │ FastAPI SQLModel │ +│ fastapi-llm-agent │ FastAPI LLM Agent │ └────────────────────────┴───────────────────────────────────────────────────────┘ ```
+Modèles disponibles : + +| Modèle | Description | Remarques | +|---|---|---| +| [`fastapi-default`](https://github.com/bnbong/FastAPI-fastkit/blob/main/src/fastapi_fastkit/fastapi_project_template/fastapi-default/README.md-tpl) | Projet FastAPI simple avec une disposition classique en couches | Bon premier choix | +| [`fastapi-empty`](https://github.com/bnbong/FastAPI-fastkit/blob/main/src/fastapi_fastkit/fastapi_project_template/fastapi-empty/README.md-tpl) | Modèle FastAPI minimal | Base du préréglage `minimal` | +| [`fastapi-single-module`](https://github.com/bnbong/FastAPI-fastkit/blob/main/src/fastapi_fastkit/fastapi_project_template/fastapi-single-module/README.md-tpl) | Application FastAPI en un seul fichier | Base du préréglage `single-module` | +| [`fastapi-domain-starter`](https://github.com/bnbong/FastAPI-fastkit/blob/main/src/fastapi_fastkit/fastapi_project_template/fastapi-domain-starter/README.md-tpl) | Starter orienté domaine, centré sur pyproject, pour les API de taille moyenne | Base du préréglage `domain-starter` | +| [`fastapi-auth-jwt`](https://github.com/bnbong/FastAPI-fastkit/blob/main/src/fastapi_fastkit/fastapi_project_template/fastapi-auth-jwt/README.md-tpl) | Authentification JWT avec rotation des jetons de rafraîchissement, hachage argon2 et portées (scopes) | Nouveau dans v1.4.0 | +| [`fastapi-sqlmodel`](https://github.com/bnbong/FastAPI-fastkit/blob/main/src/fastapi_fastkit/fastapi_project_template/fastapi-sqlmodel/README.md-tpl) | SQLModel asynchrone + migrations Alembic + CRUD générique avec pagination | Nouveau dans v1.4.0 | +| [`fastapi-llm-agent`](https://github.com/bnbong/FastAPI-fastkit/blob/main/src/fastapi_fastkit/fastapi_project_template/fastapi-llm-agent/README.md-tpl) | Agent de chat Claude en streaming (SSE) avec une boucle d'appels d'outils | Nouveau dans v1.4.0 | +| [`fastapi-mcp`](https://github.com/bnbong/FastAPI-fastkit/blob/main/src/fastapi_fastkit/fastapi_project_template/fastapi-mcp/README.md-tpl) | Application FastAPI exposée comme serveur MCP | | +| [`fastapi-psql-orm`](https://github.com/bnbong/FastAPI-fastkit/blob/main/src/fastapi_fastkit/fastapi_project_template/fastapi-psql-orm/README.md-tpl) | API de gestion d'éléments avec PostgreSQL, SQLModel, Alembic et docker-compose | Nécessite Docker | +| [`fastapi-custom-response`](https://github.com/bnbong/FastAPI-fastkit/blob/main/src/fastapi_fastkit/fastapi_project_template/fastapi-custom-response/README.md-tpl) | API de gestion d'éléments avec une enveloppe de réponse personnalisée, gestion des erreurs et pagination | | +| [`fastapi-async-crud`](https://github.com/bnbong/FastAPI-fastkit/blob/main/src/fastapi_fastkit/fastapi_project_template/fastapi-async-crud/README.md-tpl) | API asynchrone de gestion d'éléments | **Obsolète** — utilisez `fastapi-sqlmodel` | +| [`fastapi-dockerized`](https://github.com/bnbong/FastAPI-fastkit/blob/main/src/fastapi_fastkit/fastapi_project_template/fastapi-dockerized/README.md-tpl) | API de gestion d'éléments conteneurisée avec Docker | **Obsolète** — utilisez `fastapi-default` avec l'option Docker de `init --interactive` | + +Les modèles obsolètes fonctionnent toujours, mais ne sont plus des points de départ recommandés. Consultez [Quel starter choisir ?](user-guide/choosing-a-starter.md) pour un guide de décision. + ## Documentation Pour des guides complets et des instructions d'utilisation détaillées, explorez notre documentation : +- 🧭 **[Quel starter choisir ?](user-guide/choosing-a-starter.md)** — guide de décision pour débutants entre les modèles `startdemo` et les préréglages interactifs - 📚 **[Guide de l'utilisateur](user-guide/quick-start.md)** — guides détaillés d'installation et d'utilisation - 🎯 **[Tutoriel](tutorial/getting-started.md)** — tutoriels pas à pas pour les débutants - 📖 **[Référence CLI](user-guide/cli-reference.md)** — référence complète des commandes +- 🧱 **[Matrice des préréglages d'architecture](reference/preset-feature-matrix.md)** — contrat par préréglage et par fonctionnalité pour la génération interactive - 🔍 **[Assurance qualité des modèles](reference/template-quality-assurance.md)** — tests automatisés et standards de qualité ## 🚀 Tutoriels basés sur les modèles @@ -486,16 +565,19 @@ Apprenez FastAPI à travers des cas pratiques grâce à nos modèles déjà prê - **[Construire un serveur d'API basique](tutorial/basic-api-server.md)** — créez votre premier serveur FastAPI à l'aide du modèle `fastapi-default` - **[Construire une API CRUD asynchrone](tutorial/async-crud-api.md)** — développez une API asynchrone haute performance avec le modèle `fastapi-async-crud` - **[Projet orienté domaine (Domain Starter)](tutorial/domain-starter.md)** — construisez une API de taille moyenne avec le modèle `fastapi-domain-starter`, l'option moderne recommandée +- **[Authentification JWT](tutorial/auth-jwt.md)** — ajoutez de vrais comptes avec le modèle `fastapi-auth-jwt` : jetons de rafraîchissement renouvelés par rotation, hachage argon2id, gardes de rôle et de portée ### 🗄️ Base de données et infrastructure - **[Intégration avec une base de données](tutorial/database-integration.md)** — exploitez PostgreSQL + SQLAlchemy avec le modèle `fastapi-psql-orm` -- **[Conteneurisation Docker et déploiement](tutorial/docker-deployment.md)** — configurez un environnement de déploiement en production avec le modèle `fastapi-dockerized` +- **[Persistance asynchrone avec SQLModel](tutorial/sqlmodel.md)** — SQLModel asynchrone, migrations Alembic, base CRUD générique et listes paginées avec le modèle `fastapi-sqlmodel` +- **[Conteneurisation Docker et déploiement](tutorial/docker-deployment.md)** — configurez un environnement de déploiement en production avec le modèle `fastapi-dockerized` *(modèle obsolète — conservé pour les utilisateurs existants)* ### ⚡ Fonctionnalités avancées - **[Gestion des réponses personnalisées et conception d'API avancée](tutorial/custom-response-handling.md)** — construisez des API d'entreprise avec le modèle `fastapi-custom-response` - **[Intégration avec MCP](tutorial/mcp-integration.md)** — créez un serveur d'API intégré à des modèles d'IA avec le modèle `fastapi-mcp` +- **[Agent LLM en streaming](tutorial/llm-agent.md)** — construisez un backend de chat Claude en streaming avec une véritable boucle d'appels d'outils grâce au modèle `fastapi-llm-agent` Chaque tutoriel propose : diff --git a/docs/fr/reference/preset-feature-matrix.md b/docs/fr/reference/preset-feature-matrix.md index de02801..0c433d7 100644 --- a/docs/fr/reference/preset-feature-matrix.md +++ b/docs/fr/reference/preset-feature-matrix.md @@ -29,12 +29,18 @@ Ces fonctionnalités sont prises en charge dans **tous** les préréglages — l | Fonctionnalité | `minimal` / `single-module` | `classic-layered` / `domain-starter` | |---|---|---| | **Base de données** (PostgreSQL, MySQL, SQLite, MongoDB) | Génère le module de configuration **et** insère des appels `await init_db()` dans le `main.py` régénéré. | Génère le module de configuration au chemin du préréglage. Le `main.py` livré est **préservé**, câblez donc manuellement `get_db()` dans les routeurs. | -| **Authentification** (JWT, FastAPI-Users, OAuth2, basée sur session) | Génère le module de configuration d'authentification. JWT importe aussi `HTTPBearer` dans le `main.py` régénéré. | Génère le module de configuration d'authentification au chemin du préréglage. Aucun import n'est ajouté à `main.py` — câblez les dépendances manuellement. | -| **Tâches en arrière-plan** (Celery, Dramatiq) | Paquets installés ; aucune superposition de main.py aujourd'hui. | Idem. | -| **Mise en cache** (Redis) | Paquets installés ; aucune superposition de main.py aujourd'hui. | Idem. | +| **Authentification** (JWT, FastAPI-Users, OAuth2, basée sur session) | Génère le module de configuration d'authentification ; JWT importe `HTTPBearer`, et OAuth2 ainsi que l'authentification basée sur session ajoutent aussi leur configuration de middleware dans `main.py`. | Génère le module de configuration d'authentification au chemin du préréglage. Aucun import n'est ajouté à `main.py` — câblez les dépendances manuellement. | +| **Tâches en arrière-plan** (Celery, Dramatiq) | Génère `worker.py` et `features/tasks.py`, et monte le routeur de tâches dans le `main.py` régénéré. | Génère `worker.py` et `features/tasks.py`, mais le `main.py` livré est **préservé** — montez le routeur manuellement. | +| **Mise en cache** (Redis) | Génère `features/cache.py` et le monte / l'initialise dans le `main.py` régénéré. | Génère `features/cache.py` ; montez-le manuellement. | +| **Utilitaires : Pagination / WebSocket** | Génère `features/pagination.py` / `features/websocket.py` et monte le routeur dans le `main.py` régénéré. | Génère le même module ; montez-le manuellement. | +| **Supervision : OpenTelemetry** | Aucun fichier autonome — la configuration du traçage est ajoutée directement au `main.py` régénéré. | Aucun fichier autonome. Le `main.py` livré est préservé, le traçage n'est donc **pas câblé** — ajoutez la configuration manuellement. | +| **Migrations** (Alembic) | Génère `alembic.ini`, `alembic/env.py`, une révision de référence et `scripts/migrate.sh` pour toute sélection de base de données SQL — indépendamment de la superposition de `main.py`. | Idem — la génération Alembic ne dépend pas de la préservation de `main.py`. | +| **Outillage** (ruff, pre-commit, github-actions, devcontainer, makefile) | Génère le ou les fichiers de configuration correspondants, indépendamment de la superposition de `main.py`. | Idem. | +| **Journalisation** (structured) | Génère `logging_config.py` et câble le middleware d'identifiant de requête dans le `main.py` régénéré. | Génère `logging_config.py` ; le `main.py` livré est préservé, câblez donc le middleware manuellement. | | **CORS** (utilitaire) | `CORSMiddleware` est ajouté au `main.py` régénéré avec `allow_origins=['*']`. | **Déjà câblé** dans le `main.py` livré (conditionné par `settings.all_cors_origins`). Activez-le en définissant `BACKEND_CORS_ORIGINS` dans `.env` — aucune modification de code nécessaire. | -| **Tests** (Basic / Coverage / Advanced) | `pytest.ini` est généré à la racine du projet. | Idem. | +| **Tests** (Basic / Coverage / Advanced) | `pytest.ini` est généré à la racine du projet ; Advanced ajoute aussi `tests/factories.py` et `tests/test_factories.py`. | Idem. | | **Déploiement** (Docker, docker-compose) | `Dockerfile` et/ou `docker-compose.yml` écrits à la racine du projet. | Idem. | +| **`/health` / `/ready`** | Toujours présents dans le `main.py` régénéré. | Déjà présents dans le `main.py` préservé, livré par le modèle. | ## Quand un avertissement « Préréglage compatibilité » apparaît @@ -44,10 +50,15 @@ Pour les préréglages qui **préservent le `main.py` livré** (`classic-layered |---|---| | `CORS` (utilitaire) | ❌ — déjà câblé dans le `main.py` livré. Renseignez simplement `BACKEND_CORS_ORIGINS` dans `.env`. | | `Rate-Limiting` (utilitaire) | ✅ — la configuration du limiteur `slowapi` n'est pas ajoutée | -| `Prometheus` (supervision) | ✅ — `Instrumentator().instrument(app)` n'est pas appelé | +| `Pagination` / `WebSocket` (utilitaire) | ✅ — le routeur généré (`features/pagination.py` / `features/websocket.py`) n'est pas monté | +| `Celery` / `Dramatiq` (async_tasks) | ✅ — le routeur généré `worker.py` / `features/tasks.py` n'est pas monté | +| `Redis` (mise en cache) | ✅ — le `features/cache.py` généré n'est ni monté ni initialisé | +| `Prometheus` / `OpenTelemetry` (supervision) | ✅ — la configuration de l'instrumentation n'est pas ajoutée à `main.py` | +| `structured` (journalisation) | ✅ — `logging_config.py` est généré, mais le middleware d'identifiant de requête n'est pas ajouté à `main.py` | +| `Alembic` (migrations), sélections d'outillage | ❌ — ces fichiers sont générés et utilisables quel que soit le préréglage ; rien ne dépend de `main.py` | | Toute sélection base de données / authentification | ⚠️ — les fichiers de configuration sont générés, mais vous devez les passer en `Depends()` dans vos routeurs | -Pour les préréglages `minimal` et `single-module`, la superposition dynamique de `main.py` gère automatiquement CORS, la limitation de débit et l'instrumentation Prometheus ; aucun avertissement n'est émis. +Pour les préréglages `minimal` et `single-module`, la superposition dynamique de `main.py` gère automatiquement chacun des axes ci-dessus ; aucun avertissement n'est émis. ## Combinaisons non prises en charge (rester prudent) diff --git a/docs/fr/reference/template-quality-assurance.md b/docs/fr/reference/template-quality-assurance.md index 87b4050..f5f6630 100644 --- a/docs/fr/reference/template-quality-assurance.md +++ b/docs/fr/reference/template-quality-assurance.md @@ -21,6 +21,38 @@ Chaque mercredi à minuit (UTC), notre workflow GitHub Actions inspecte automati - ✅ **Vérification des dépendances** — confirme que FastAPI et les dépendances requises sont correctement définies - ✅ **Implémentation FastAPI** — vérifie que les modèles contiennent une initialisation correcte de l'application FastAPI - ✅ **Exécution des tests** — exécute les tests du modèle pour s'assurer du bon fonctionnement +- ✅ **Test smoke** — démarre le projet généré avec uvicorn et sonde sa surface HTTP +- ✅ **Cohérence de la configuration** — épinglages de version Python, divergence des dépendances, auto-dépendance +- ✅ **Résidus d'emplacements** — aucun `` ne subsiste après la génération du projet +- ✅ **Fraîcheur des dépendances** — signale les épinglages en retard sur leur dernière version publiée sur PyPI + +## Ce que fait chaque vérification + +L'inspection génère un **véritable projet** à partir du modèle — copie, conversion des fichiers `-tpl`, injection des métadonnées — puis exécute chaque vérification sur ce projet généré, et non sur la source du modèle. Une vérification qui ne lit que le modèle ne peut pas détecter un emplacement que la génération n'a pas réussi à substituer. + +### Test smoke + +Le projet généré est démarré avec uvicorn sur un port libre, et l'inspecteur interroge le serveur jusqu'à ce qu'il réponde ou que le délai expire. Une fois le serveur opérationnel, `/docs` et `/health` sont appelés et leurs codes de statut vérifiés. Le processus serveur est ensuite arrêté, avec escalade vers `SIGKILL` s'il ne s'arrête pas. + +La cible uvicorn est résolue dans l'ordre suivant : `[tool.fastapi-fastkit].app_module` dans le `pyproject.toml` généré, puis `app_module` dans le `template-config.yml` du modèle, puis les emplacements conventionnels de `main.py`. C'est le même contrat que celui utilisé par `fastkit runserver` : un modèle dont le test smoke démarre est donc un modèle dont le projet généré s'exécute. + +C'est la vérification qui détecte ce que l'analyse statique ne peut pas voir : un import qui ne se résout qu'à l'exécution, un `lifespan` qui lève une exception, un champ de configuration sans valeur par défaut. + +### Cohérence de la configuration + +Trois points sont validés par rapport au `pyproject.toml-tpl` du modèle : + +- **Les épinglages de version Python concordent et ciblent 3.12** — `requires-python`, le `target-version` de black et le `python_version` de mypy. Un modèle qui formate en `py311` tout en déclarant `>=3.12` est un bug qui attend de tomber sur un contributeur. +- **Aucune divergence des dépendances** — les paquets déclarés dans `requirements.txt-tpl` et dans `[project].dependencies` ne doivent pas se contredire. Les modèles fournissent les deux fichiers pour la compatibilité avec les gestionnaires de paquets, et deux listes finissent toujours par diverger si rien ne les vérifie. +- **Aucune auto-dépendance** — un projet généré ne doit pas déclarer `FastAPI-fastkit` comme dépendance d'exécution. Le CLI est un outil de développement ; l'embarquer dans chaque application générée ferait entrer tout l'arbre de dépendances de fastkit dans des projets sans rapport. + +### Résidus d'emplacements + +Chaque ``, `` et consorts doit être substitué pendant la génération. Tout `` encore présent dans le projet généré est une erreur — cela signifie que l'injection des métadonnées ne connaît pas ce fichier, et que l'utilisateur le recevrait tel quel. + +### Fraîcheur des dépendances + +Les versions épinglées sont comparées à la dernière version publiée sur PyPI et signalées comme avertissements, jamais comme erreurs — un modèle en retard d'une version mineure est une information, pas un échec. C'est la seule vérification qui accède au réseau, et `--offline` l'ignore. ## Système de tests automatisés des modèles @@ -140,6 +172,30 @@ $ python scripts/inspect-templates.py --verbose $ python scripts/inspect-templates.py --output my_results.json ``` +#### Options d'inspection + +| Option | Effet | Quand l'utiliser | +|---|---|---| +| `--offline` | Ignore toutes les requêtes réseau (rapport de fraîcheur des dépendances) | Pas de réseau, ou vous voulez une exécution rapide et hermétique | +| `--no-smoke` | Ignore le démarrage du projet généré avec uvicorn | Vous itérez uniquement sur la structure ; le test smoke est l'étape la plus lente | +| `--mypy` | Vérifie les types du projet généré avec mypy | Avant de soumettre un modèle — option à activer explicitement car elle est lente | +| `--templates` | Liste de modèles à inspecter, séparés par des virgules | Vous travaillez sur un seul modèle | +| `--verbose` | Sortie détaillée | Vous déboguez un échec | +| `--output` | Chemin du fichier de résultats JSON | Vous conservez plusieurs exécutions côte à côte | + +Une boucle locale rapide sur un seul modèle : + +```console +$ python scripts/inspect-templates.py \ + --templates fastapi-sqlmodel --offline --no-smoke --verbose +``` + +...et la vérification complète avant d'ouvrir une PR : + +```console +$ python scripts/inspect-templates.py --templates fastapi-sqlmodel --mypy +``` + ### Utiliser les commandes Makefile ```console @@ -192,10 +248,11 @@ Pour qu'un modèle passe l'inspection, il doit remplir ces exigences : - Doit contenir un répertoire `src/` avec des fichiers source Python - Les fichiers Python doivent utiliser l'extension `.py-tpl` - Doit inclure un répertoire `tests/` et un fichier `README.md-tpl` -- Doit inclure **au moins un** fichier de métadonnées : - - `pyproject.toml-tpl` (préféré, PEP 621), ou - - `setup.py-tpl` (legacy, toujours accepté) -- `requirements.txt-tpl` est optionnel quand `pyproject.toml-tpl` déclare `[project].dependencies` +- Doit inclure `pyproject.toml-tpl` (PEP 621). Les modèles livrés sont pyproject-first et ne fournissent plus de `setup.py-tpl` ; l'inspection l'accepte toujours pour que les modèles tiers continuent de fonctionner, mais les nouveaux modèles ne doivent pas en ajouter. +- `requirements.txt-tpl` est optionnel quand `pyproject.toml-tpl` déclare `[project].dependencies` — mais lorsque les deux sont présents, ils doivent concorder (voir *Cohérence de la configuration*) +- Doit cibler Python 3.12 de manière cohérente : `requires-python = ">=3.12"`, black `target-version = ["py312"]`, mypy `python_version = "3.12"`, et `python:3.12-slim` pour toute image Docker de base +- Ne doit pas déclarer `FastAPI-fastkit` comme dépendance d'exécution +- Ne doit laisser aucun `` non substitué après la génération ### Exigences FastAPI - Doit contenir l'initialisation de l'application FastAPI diff --git a/docs/fr/reference/translation-status.md b/docs/fr/reference/translation-status.md index fc06101..79c6f70 100644 --- a/docs/fr/reference/translation-status.md +++ b/docs/fr/reference/translation-status.md @@ -18,15 +18,15 @@ Les nombres ci-dessous comptent les pages Markdown dans l'arborescence de chaque | Locale | État | Pages Markdown | Notes | |---|---|---:|---| -| 🇬🇧 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`) | ✅ 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. | +| 🇬🇧 Anglais (`en`) | ✅ Source de vérité | 29 / 29 | Référence. | +| 🇰🇷 Coréen (`ko`) | ✅ Complet | 29 / 29 | Phases 1 à 3 terminées. Resynchronisé avec les sources anglaises de la v1.4.1 le 2026-10-11 : les trois tutoriels de la v1.4.0 (`tutorial/auth-jwt.md`, `tutorial/sqlmodel.md`, `tutorial/llm-agent.md`) sont traduits, et les pages qui s'étaient écartées de la source depuis leur traduction initiale ont été remises à niveau. `docs/ko/changelog.md` réutilise intentionnellement le `CHANGELOG.md` canonique en anglais. | +| 🇯🇵 Japonais (`ja`) | ✅ Complet | 29 / 29 | Phases 1 à 3 terminées. Resynchronisé avec les sources anglaises de la v1.4.1 le 2026-10-11 : les trois tutoriels de la v1.4.0 (`tutorial/auth-jwt.md`, `tutorial/sqlmodel.md`, `tutorial/llm-agent.md`) sont traduits, et les pages qui s'étaient écartées de la source depuis leur traduction initiale ont été remises à niveau. `docs/ja/changelog.md` réutilise intentionnellement le `CHANGELOG.md` canonique en anglais. | +| 🇨🇳 Chinois (`zh`) | ✅ Complet | 29 / 29 | Phases 1 à 3 terminées. Resynchronisé avec les sources anglaises de la v1.4.1 le 2026-10-11 : les trois tutoriels de la v1.4.0 (`tutorial/auth-jwt.md`, `tutorial/sqlmodel.md`, `tutorial/llm-agent.md`) sont traduits, et les pages qui s'étaient écartées de la source depuis leur traduction initiale ont été remises à niveau. `docs/zh/changelog.md` réutilise intentionnellement le `CHANGELOG.md` canonique en anglais. | +| 🇪🇸 Espagnol (`es`) | ✅ Complet | 29 / 29 | Phases 1 à 3 terminées. Resynchronisé avec les sources anglaises de la v1.4.1 le 2026-10-11 : les trois tutoriels de la v1.4.0 (`tutorial/auth-jwt.md`, `tutorial/sqlmodel.md`, `tutorial/llm-agent.md`) sont traduits, et les pages qui s'étaient écartées de la source depuis leur traduction initiale ont été remises à niveau. `docs/es/changelog.md` réutilise intentionnellement le `CHANGELOG.md` canonique en anglais. | +| 🇫🇷 Français (`fr`) | ✅ Complet | 29 / 29 | Phases 1 à 3 terminées. Resynchronisé avec les sources anglaises de la v1.4.1 le 2026-10-11 : les trois tutoriels de la v1.4.0 (`tutorial/auth-jwt.md`, `tutorial/sqlmodel.md`, `tutorial/llm-agent.md`) sont traduits, et les pages qui s'étaient écartées de la source depuis leur traduction initiale ont été remises à niveau. `docs/fr/changelog.md` réutilise intentionnellement le `CHANGELOG.md` canonique en anglais. | +| 🇩🇪 Allemand (`de`) | ✅ Complet | 29 / 29 | Phases 1 à 3 terminées. Resynchronisé avec les sources anglaises de la v1.4.1 le 2026-10-11 : les trois tutoriels de la v1.4.0 (`tutorial/auth-jwt.md`, `tutorial/sqlmodel.md`, `tutorial/llm-agent.md`) sont traduits, et les pages qui s'étaient écartées de la source depuis leur traduction initiale ont été remises à niveau. `docs/de/changelog.md` réutilise intentionnellement le `CHANGELOG.md` canonique en anglais. | -*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 : +*Instantané vérifié le 2026-10-11, après la publication de la v1.4.1. Les six locales traduites ont été remises à parité avec les sources anglaises : les trois tutoriels ajoutés dans la v1.4.0 ont été traduits et chaque page qui s'était écartée de la source depuis sa traduction initiale a été resynchronisée. Les traductions peuvent encore prendre du retard au cours d'une version — si une page traduite contredit la page anglaise, faites confiance à l'anglais.* 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 @@ -39,6 +39,7 @@ Si le recomptage ne correspond pas au tableau, c'est que le tableau est obsolèt Légende : - ✅ **Langue source** — la locale dans laquelle on écrit en premier. +- ✅ **Complet** — chaque page anglaise a son équivalent traduit. Certaines pages peuvent encore prendre du retard sur une mise à jour anglaise au cours d'une version. - 🟡 **Partiel** — certaines pages sont traduites ; les pages manquantes retombent sur l'anglais. - 🔴 **Squelette** — l'entrée du sélecteur de langue existe, mais aucune page traduite n'est encore livrée. Le site rend le contenu anglais sous les libellés de navigation traduits. diff --git a/docs/fr/tutorial/auth-jwt.md b/docs/fr/tutorial/auth-jwt.md new file mode 100644 index 0000000..b78f83c --- /dev/null +++ b/docs/fr/tutorial/auth-jwt.md @@ -0,0 +1,249 @@ +# Authentification JWT avec `fastapi-auth-jwt` + +Construisez une API dotée de véritables comptes dès le premier commit. Ce tutoriel parcourt le modèle `fastapi-auth-jwt` de bout en bout : comment les paires de jetons sont émises et renouvelées par rotation, pourquoi un jeton de rafraîchissement rejoué est refusé, comment les gardes de rôle et de portée (scope) sont câblées, et ce qui change lorsque vous quittez SQLite. + +## Ce que vous apprendrez + +- Générer un projet avec `fastkit startdemo fastapi-auth-jwt` +- La différence entre le jeton d'accès et le jeton de rafraîchissement, et pourquoi la rotation est importante +- Comment `get_current_user`, `get_current_active_superuser` et `require_scopes(...)` protègent une route +- Où a lieu le hachage des mots de passe et pourquoi il repose sur argon2id +- Passer du fichier SQLite fourni à PostgreSQL avec Alembic + +## Prérequis + +- Python 3.12+ +- FastAPI-fastkit installé (`pip install fastapi-fastkit`) +- À l'aise avec les dépendances FastAPI et les schémas pydantic + +Si vous n'avez encore jamais construit d'application FastAPI, commencez par [Construire un serveur d'API basique](basic-api-server.md) puis revenez ici — ce modèle suppose les bases acquises. + +## Étape 1 : Générer le projet + +```console +$ fastkit startdemo fastapi-auth-jwt +Enter the project name: accounts-api +Enter the author name: Developer Kim +Enter the author email: developer@example.com +Enter the project description: Accounts and sessions service +Select package manager (pip, uv, pdm, poetry) [uv]: uv +Do you want to proceed with project creation? [y/N]: y +``` + +Lancez-le ensuite : + +```console +$ cd accounts-api +$ bash scripts/run-server.sh # or: uvicorn src.app.main:app --reload +``` + +L'application démarre sur SQLite (`sqlite:///./app.db`) et crée ses tables au démarrage : il n'y a donc rien à installer ni à configurer au préalable. La documentation est disponible à . + +!!! tip "Prévisualiser avant de vous engager" + `fastkit startdemo fastapi-auth-jwt --dry-run` affiche l'arborescence des fichiers et la liste des paquets sans rien écrire — pratique pour voir ce que vous allez obtenir. + +## Étape 2 : L'arborescence générée + +``` +accounts-api/ +├── pyproject.toml # métadonnées PEP 621 + [tool.fastapi-fastkit] +├── requirements.txt +├── alembic.ini +├── Dockerfile +├── .env # SECRET_KEY, DATABASE_URL, durées de vie des jetons +├── scripts/ +│ ├── format.sh lint.sh run-server.sh test.sh +├── src/ +│ └── app/ +│ ├── main.py # FastAPI() + lifespan + CORS + api_router +│ ├── core/ +│ │ ├── config.py # pydantic-settings +│ │ └── security.py # hachage argon2 + encodage/décodage JWT +│ ├── db/session.py # moteur + dépendance get_session +│ ├── api/ +│ │ ├── router.py # regroupe la santé et tous les routeurs de domaine +│ │ ├── health.py # GET /health +│ │ └── deps.py # session, utilisateur courant, gardes de rôle/portée +│ ├── domains/ +│ │ ├── auth/ # connexion, rotation du rafraîchissement, déconnexion +│ │ └── users/ # inscription et profils +│ └── alembic/ # environnement de migration + versions/ +└── tests/ + ├── conftest.py test_health.py test_auth.py test_users.py +``` + +La disposition reprend la forme de `domain-starter` : chaque domaine possède son `router.py` (transport), son `service.py` (logique métier), son `repository.py` (accès aux données), son `models.py` (tables) et son `schemas.py` (entrées/sorties de l'API). Les préoccupations d'authentification vivent dans `auth/`, celles des comptes dans `users/`, et aucun des deux ne touche aux éléments internes de l'autre. + +## Étape 3 : S'inscrire, se connecter, appeler une route protégée + +```console +# 1. register +$ curl -X POST http://127.0.0.1:8000/api/v1/auth/register \ + -H 'Content-Type: application/json' \ + -d '{"email": "alice@example.com", "password": "s3cret-password"}' + +# 2. log in — note the form encoding: this is the OAuth2 password flow +$ curl -X POST http://127.0.0.1:8000/api/v1/auth/login \ + -d 'username=alice@example.com&password=s3cret-password' +{"access_token": "...", "refresh_token": "...", "token_type": "bearer"} + +# 3. call a protected route +$ curl http://127.0.0.1:8000/api/v1/users/me \ + -H "Authorization: Bearer $ACCESS_TOKEN" +``` + +Le bouton « Authorize » de Swagger pilote le même flux : vous pouvez donc exercer les routes protégées depuis `/docs` sans quitter le navigateur. + +L'ensemble des points d'extrémité : + +| Méthode | Point d'extrémité | Auth | Description | +|---------|-----------------------------|-------------|----------------------------------------------| +| GET | `/api/v1/health` | — | Sonde de vivacité | +| POST | `/api/v1/auth/register` | — | Créer un compte | +| POST | `/api/v1/auth/login` | — | Formulaire de mot de passe OAuth2 → paire de jetons | +| POST | `/api/v1/auth/refresh` | refresh tok | Rotation vers une nouvelle paire de jetons | +| POST | `/api/v1/auth/logout` | refresh tok | Révoquer un jeton de rafraîchissement | +| POST | `/api/v1/auth/logout-all` | access tok | Révoquer toutes les sessions de l'utilisateur | +| GET | `/api/v1/users/me` | access tok | Profil de l'utilisateur courant | +| PATCH | `/api/v1/users/me` | access tok | Mettre à jour le profil de l'utilisateur courant | +| GET | `/api/v1/users/me/scopes` | scope `me` | Route d'exemple conditionnée par une portée | +| GET | `/api/v1/users` | superuser | Liste des utilisateurs conditionnée par un rôle | + +## Étape 4 : Deux jetons, et pourquoi + +Une connexion renvoie une **paire** : + +- Le **jeton d'accès** a une durée de vie courte (`ACCESS_TOKEN_EXPIRE_MINUTES`, 30 par défaut) et accompagne chaque requête. Comme sa durée de vie est courte, un jeton divulgué cesse rapidement de fonctionner. +- Le **jeton de rafraîchissement** a une durée de vie longue (`REFRESH_TOKEN_EXPIRE_DAYS`, 14 par défaut) et ne sert qu'à une seule chose : obtenir une nouvelle paire. + +Rien de tout cela n'est inhabituel. Ce qui distingue ce modèle de l'extrait JWT moyen, c'est que les jetons de rafraîchissement sont **suivis et renouvelés par rotation**. Chaque jeton de rafraîchissement émis reçoit une revendication `jti`, et ce `jti` est écrit en base de données : + +```python +def issue_token_pair(self, user: User) -> TokenPair: + scopes = user.scope_list + access_token, _, _ = create_access_token(str(user.id), scopes) + refresh_token, jti, expires_at = create_refresh_token(str(user.id), scopes) + self.refresh_tokens.add(jti, user.id or 0, expires_at) + return TokenPair(access_token=access_token, refresh_token=refresh_token) +``` + +Lors d'un rafraîchissement, le jeton présenté est révoqué **avant** l'émission de la nouvelle paire : + +```python +def rotate(self, refresh_token: str) -> TokenPair: + payload = decode_token(refresh_token, REFRESH_TOKEN_TYPE) + record = self.refresh_tokens.get_by_jti(str(payload.get("jti"))) + if record is None or record.revoked: + raise TokenError("refresh token is no longer valid") + ... + self.refresh_tokens.revoke(record) + return self.issue_token_pair(user) +``` + +Essayez : rafraîchissez une fois, puis renvoyez l'*ancien* jeton de rafraîchissement. + +```console +$ curl -X POST http://127.0.0.1:8000/api/v1/auth/refresh \ + -H 'Content-Type: application/json' \ + -d "{\"refresh_token\": \"$REFRESH_TOKEN\"}" +# → new pair + +$ curl -X POST http://127.0.0.1:8000/api/v1/auth/refresh \ + -H 'Content-Type: application/json' \ + -d "{\"refresh_token\": \"$REFRESH_TOKEN\"}" +# → 401: the token died the moment it was used +``` + +C'est cette seule propriété qui transforme un jeton de rafraîchissement volé, porte dérobée permanente, en un échec détectable et à usage unique. + +La déconnexion fonctionne de la même manière, ce qui est l'autre chose que les configurations JWT classiques ne savent généralement pas faire : `POST /auth/logout` révoque un jeton, `POST /auth/logout-all` révoque tous les jetons de rafraîchissement appartenant à l'utilisateur. + +## Étape 5 : Les mots de passe + +Le hachage se trouve dans `src/app/core/security.py` et passe par `pwdlib` avec **argon2id** — le choix par défaut recommandé aujourd'hui, et non une ancienne configuration bcrypt. Aucun routeur ni service ne voit jamais un mot de passe en clair au-delà de l'instant où il est vérifié, et `hashed_password` ne fait jamais partie d'un schéma de réponse. + +Vous n'avez rien à configurer ici. La seule chose que vous *devez* configurer est la clé de signature. + +## Étape 6 : Rôles et portées + +Trois gardes sont fournies sous forme de dépendances FastAPI : + +```python +CurrentUser = Annotated[User, Depends(get_current_user)] +``` + +- **`get_current_user`** décode le jeton d'accès bearer, charge l'utilisateur et rejette les comptes inactifs avec `403`. Tout jeton malformé, expiré ou pointant vers un utilisateur supprimé renvoie `401`. +- **`get_current_active_superuser`** ajoute par-dessus une vérification `is_superuser`. `GET /api/v1/users` l'utilise. +- **`require_scopes(...)`** construit une dépendance qui exige des portées précises : + +```python +from fastapi import Depends +from src.app.api.deps import require_scopes + +@router.get("/reports", dependencies=[Depends(require_scopes("reports"))]) +def read_reports() -> list[str]: + ... +``` + +Les portées sont stockées sur la ligne de l'utilisateur sous forme de chaîne séparée par des virgules (`scopes: str`, relue via la propriété `scope_list`) et sont copiées dans chaque jeton émis. C'est délibérément la solution la plus simple qui fonctionne pour un starter ; lorsque votre modèle de permissions dépasse ce qu'une chaîne peut exprimer, remplacez la colonne par une table de rôles et conservez les signatures des dépendances inchangées — rien n'a besoin de bouger dans les routeurs. + +## Étape 7 : Ajouter un domaine protégé + +Pour ajouter, par exemple, un domaine `orders` accessible uniquement aux utilisateurs connectés : + +1. Créez `src/app/domains/orders/` sur le modèle de `users/` (`models.py`, `schemas.py`, `repository.py`, `service.py`, `router.py`). +2. Prenez `CurrentUser` en paramètre dans les routes qui nécessitent un compte, ou attachez `Depends(require_scopes("orders"))` au routeur. +3. Enregistrez le routeur dans `src/app/api/router.py`, au-dessus de l'ancre `# fastkit:routes`. `fastkit addroute` écrit dans ces mêmes ancres, si bien que les routes écrites à la main et les routes générées restent au même endroit. +4. Générez une migration : `alembic revision --autogenerate -m "add orders"`. +5. Calquez `tests/test_users.py` pour les nouveaux points d'extrémité. + +## Étape 8 : SQLite maintenant, PostgreSQL plus tard + +La `DATABASE_URL` par défaut est un fichier SQLite local et l'application appelle `create_db_and_tables()` au démarrage : c'est pourquoi `pytest` et `uvicorn` fonctionnent tous deux sans aucune configuration. Pour tout ce qui dépasse le développement local, laissez plutôt Alembic gérer le schéma : + +```console +$ export DATABASE_URL='postgresql+psycopg://postgres:postgres@localhost:5432/app_db' +$ alembic upgrade head +``` + +Après une modification de modèle : + +```console +$ alembic revision --autogenerate -m "add profile fields" +``` + +## Étape 9 : Avant de déployer + +Deux paramètres de `.env` décident s'il s'agit d'une démo ou d'un vrai service : + +- **`SECRET_KEY`** signe chaque jeton. Si vous la laissez vide, une clé aléatoire est générée pour chaque processus — ce qui déconnecte tout le monde à chaque redémarrage et fait que plusieurs workers ne s'accordent pas sur les jetons valides. Générez-en une et fixez-la : + + ```console + $ python -c "import secrets; print(secrets.token_urlsafe(32))" + ``` + +- **`DATABASE_URL`** sélectionne la base de données. La valeur SQLite par défaut n'est pas un stockage de production. + +Vous pouvez aussi définir `FIRST_SUPERUSER_EMAIL` et `FIRST_SUPERUSER_PASSWORD` pour qu'un superutilisateur soit créé au démarrage, et `BACKEND_CORS_ORIGINS` avec une liste d'origines front-end séparées par des virgules. + +## Étape 10 : Les tests + +```console +$ bash scripts/test.sh # or: pytest +``` + +La suite exécute `httpx.AsyncClient` contre une base SQLite en mémoire et couvre tout le parcours nominal — inscription → connexion → me → rafraîchissement → déconnexion — ainsi que les chemins de rejet qui comptent vraiment : mauvais mot de passe, jeton malformé, jeton expiré, et le jeton de rafraîchissement rejoué de l'étape 4. Lorsque vous étendez les règles d'authentification, étendez d'abord ces tests de rejet ; ce sont eux qui détectent une garde défaillante. + +## Récapitulatif + +- **Génération** : `fastkit startdemo fastapi-auth-jwt` → `bash scripts/run-server.sh` → documentation à `/docs`. +- **Jetons** : jeton d'accès à courte durée de vie pour les requêtes, jeton de rafraîchissement renouvelé par rotation et suivi par `jti`, de sorte que les rejeux échouent et que la déconnexion est réelle. +- **Gardes** : `get_current_user`, `get_current_active_superuser` et `require_scopes(...)` sous forme de simples dépendances. +- **Stockage** : SQLite sans effort, PostgreSQL en définissant `DATABASE_URL` et en exécutant `alembic upgrade head`. +- **Avant de déployer** : fixez `SECRET_KEY`, pointez `DATABASE_URL` vers une vraie base de données. + +## Pour aller plus loin + +- [Persistance asynchrone avec SQLModel](sqlmodel.md) — la même disposition par domaines, centrée sur l'accès asynchrone aux données, les migrations et le CRUD générique. +- [FastAPI orienté domaine](domain-starter.md) — la disposition sur laquelle repose ce modèle, sans la machinerie d'authentification. +- [Quel starter choisir ?](../user-guide/choosing-a-starter.md) — comment ce modèle se compare aux autres. diff --git a/docs/fr/tutorial/llm-agent.md b/docs/fr/tutorial/llm-agent.md new file mode 100644 index 0000000..2a4d6d0 --- /dev/null +++ b/docs/fr/tutorial/llm-agent.md @@ -0,0 +1,198 @@ +# Agent LLM en streaming avec `fastapi-llm-agent` + +Construisez un backend de chat qui diffuse les jetons au fur et à mesure de leur génération et qui peut réellement *agir* — appeler des outils, lire leurs résultats et continuer. Ce tutoriel parcourt le modèle `fastapi-llm-agent` : le protocole d'événements SSE, la boucle d'appels d'outils et son plafond d'itérations, la mémoire de conversation derrière une interface, et une suite de tests qui ne touche jamais au réseau. + +## Ce que vous apprendrez + +- Générer un projet avec `fastkit startdemo fastapi-llm-agent` +- Les types d'événements SSE qu'un client doit gérer, et ce que chacun signifie +- Comment fonctionne, étape par étape, un tour de la boucle d'appels d'outils +- Ajouter votre propre outil +- Remplacer le stockage de conversations en mémoire du processus par un vrai stockage +- Pourquoi les tests sont déterministes et gratuits à exécuter + +## Prérequis + +- Python 3.12+ +- FastAPI-fastkit installé (`pip install fastapi-fastkit`) +- Une clé d'API Anthropic — uniquement pour dialoguer avec un vrai modèle. La suite de tests générée s'exécute sans clé. +- Être familier avec `async`/`await` et les générateurs asynchrones + +## Étape 1 : Générer et configurer + +```console +$ fastkit startdemo fastapi-llm-agent +Enter the project name: support-agent +Enter the author name: Developer Kim +Enter the author email: developer@example.com +Enter the project description: Streaming support assistant +Select package manager (pip, uv, pdm, poetry) [uv]: uv +Do you want to proceed with project creation? [y/N]: y +``` + +Placez votre clé dans `.env` avant le premier lancement : + +```bash +ANTHROPIC_API_KEY=sk-ant-... +LLM_MODEL=claude-sonnet-5 +LLM_MAX_TOKENS=4096 +MAX_TOOL_ITERATIONS=5 +``` + +La clé est lue par pydantic-settings et transmise au client lors de sa construction. Elle n'est jamais écrite dans le code source ni intégrée à l'image Docker. + +```console +$ cd support-agent +$ bash scripts/run-server.sh +$ bash scripts/chat.sh "What is 12 * 34?" +``` + +!!! tip "Utiliser une passerelle au lieu de l'API Anthropic" + Définissez `LLM_BASE_URL` (et `LLM_AUTH_TOKEN` si la passerelle utilise une authentification bearer) pour pointer vers LiteLLM, un proxy interne ou un serveur local. Le format d'échange reste l'API Messages d'Anthropic ; la traduction vers d'autres fournisseurs relève de la passerelle, pas de l'application. + +## Étape 2 : L'arborescence générée + +``` +support-agent/ +├── pyproject.toml # métadonnées PEP 621 + [tool.fastapi-fastkit] +├── requirements.txt +├── Dockerfile +├── .env +├── scripts/ +│ ├── chat.sh # envoie un message à un serveur en cours d'exécution +│ ├── format.sh lint.sh run-server.sh test.sh +├── src/ +│ └── app/ +│ ├── main.py # application FastAPI + lifespan +│ ├── core/config.py # pydantic-settings (la clé d'API se trouve ici) +│ ├── llm/ +│ │ ├── client.py # fabrique AsyncAnthropic +│ │ ├── tools.py # schémas d'outils + gestionnaires +│ │ └── agent.py # agent en streaming + boucle d'outils +│ ├── memory/ +│ │ ├── base.py # interface ConversationStore +│ │ └── in_memory.py # implémentation locale au processus +│ ├── schemas/chat.py # corps de requête + enveloppe d'événement SSE +│ └── api/ +│ ├── router.py # regroupe la santé et le chat +│ ├── deps.py # dépendances store / agent +│ ├── health.py # GET /health +│ └── chat.py # POST /chat (SSE), historique des conversations +└── tests/ + ├── conftest.py # faux client Anthropic scénarisé + ├── test_health.py test_chat.py test_tools.py test_memory.py +``` + +Trois frontières méritent d'être remarquées d'emblée : le **client** est créé par une fabrique (les tests peuvent donc en substituer un), la **mémoire** est une interface dotée d'une implémentation triviale (la production peut donc en substituer une), et les **outils** sont une liste de schémas plus un dictionnaire de gestionnaires (en ajouter un ne touche donc qu'un seul fichier). + +## Étape 3 : Les points d'extrémité + +| Méthode | Point d'extrémité | Description | +|---------|------------------------------|-----------------------------------| +| GET | `/api/v1/health` | Sonde de vivacité | +| POST | `/api/v1/chat` | Diffuser une réponse en SSE | +| GET | `/api/v1/conversations/{id}` | Lire l'historique enregistré | +| DELETE | `/api/v1/conversations/{id}` | Oublier une conversation | + +`POST /api/v1/chat` accepte `{"message": "...", "conversation_id": "..."}`. Omettez `conversation_id` pour démarrer une nouvelle conversation, puis récupérez l'identifiant attribué dans l'événement `done` final. + +## Étape 4 : Le protocole de flux + +Chaque message SSE porte son type dans le champ `event:` et une enveloppe JSON dans `data:`. Un client doit en gérer cinq, plus les erreurs : + +| Événement | Champs | Signification | +|---------------|---------------------------------|---------------------------------------| +| `token` | `text` | Un fragment de la réponse de l'assistant | +| `tool_use` | `tool_name`, `tool_input` | Claude a demandé un outil | +| `tool_result` | `tool_name`, `tool_output` | L'outil s'est exécuté | +| `usage` | `input_tokens`, `output_tokens` | Consommation de jetons par tour | +| `done` | `conversation_id` | Le tour est terminé | +| `error` | `detail` | Un échec est survenu en cours de flux | + +`tool_use` et `tool_result` ne sont pas décoratifs — ce sont eux qui permettent à une interface d'afficher « recherche… » au lieu d'une pause inexpliquée pendant l'exécution d'un outil. + +L'utilisation des jetons est également écrite dans le logger `app.agent` à chaque tour : la consommation est donc visible dans les journaux du serveur sans instrumenter de client. + +## Étape 5 : Un tour de boucle + +`ChatAgent.stream` dans `src/app/llm/agent.py` constitue tout le moteur. Un tour se déroule ainsi : + +1. Charger l'historique de la conversation depuis le stockage. +2. Ouvrir une requête en streaming avec l'historique, le nouveau message de l'utilisateur et les schémas d'outils. +3. Émettre chaque fragment de texte sous forme d'événement `token` dès son arrivée. +4. À la fin du flux, lire le message final ; émettre un événement `usage` et journaliser le nombre de jetons. +5. **Si `stop_reason != "tool_use"`,** l'assistant a terminé : enregistrer le tour, émettre `done`, retourner. +6. **Sinon,** pour chaque bloc `tool_use` : émettre `tool_use`, exécuter le gestionnaire, émettre `tool_result` et collecter la sortie. +7. Renvoyer tous les résultats d'outils dans un unique message `user` — c'est ce qu'exige l'API Messages — et reprendre à l'étape 2. + +La boucle est bornée par `MAX_TOOL_ITERATIONS` (5 par défaut). Si Claude continue de demander des outils au-delà du plafond, le tour est enregistré, un avertissement est journalisé et un événement `error` est émis, plutôt que de laisser le serveur tourner en boucle : + +```python +yield AgentEvent( + type="error", + detail=( + "The assistant exceeded the tool-call limit of " + f"{self._max_tool_iterations} iterations." + ), +) +``` + +Un tel plafond n'a rien de facultatif dans une boucle d'agent. Sans lui, un modèle qui comprend mal le résultat d'un outil peut vous facturer indéfiniment. + +## Étape 6 : Les outils fournis + +| Outil | Rôle | +|----------------|-------------------------------------------------------------------| +| `calculator` | Arithmétique, analysée en AST avec liste blanche — pas `eval` | +| `current_time` | Heure actuelle dans un fuseau horaire IANA | + +Le `calculator` mérite qu'on s'y attarde. Il analyse l'expression avec `ast` et ne parcourt que des types de nœuds en liste blanche : un argument d'outil provenant d'un modèle ne peut donc pas exécuter de Python arbitraire. Tout outil qui accepte une entrée générée par un modèle a besoin de la même rigueur : **la sortie d'un modèle est une entrée non fiable.** + +### Ajouter un outil + +Tout se trouve dans `src/app/llm/tools.py` : + +1. Ajoutez un schéma à `TOOL_SCHEMAS` — `name`, `description` et un `input_schema` au format JSON Schema. +2. Enregistrez le gestionnaire dans `TOOL_HANDLERS` sous le même nom. + +Deux points à soigner : + +- **La description est la seule instruction que Claude reçoit** sur le moment où recourir à l'outil. C'est de l'ingénierie de prompt, pas de la documentation — des descriptions vagues produisent des outils jamais appelés, ou appelés en permanence. +- **Les gestionnaires doivent renvoyer une chaîne et transformer leurs propres échecs en message lisible** plutôt que de lever une exception. La boucle renvoie ce texte tel quel au modèle : « aucune ville nommée X n'a été trouvée » permet au modèle de se rattraper, alors qu'une exception met fin au tour. + +## Étape 7 : La mémoire de conversation + +`ConversationStore` (`src/app/memory/base.py`) est l'interface ; le fichier `in_memory.py` fourni l'implémente avec un dictionnaire. Cela convient pour un processus unique et ne convient pour rien d'autre — un redémarrage efface tout, et deux workers ne s'accordent pas sur l'historique. + +Pour en faire un vrai stockage : implémentez la même interface au-dessus de Redis ou d'une base de données, et renvoyez-la depuis `get_store()` dans `src/app/api/deps.py`. Rien ne change dans l'agent ni dans les routeurs, car aucun des deux ne nomme jamais la classe concrète. + +## Étape 8 : Les tests + +```console +$ bash scripts/test.sh # or: pytest +``` + +`tests/conftest.py` rejoue des tours scénarisés à travers une imitation de `AsyncAnthropic.messages.stream`. La suite est ainsi déterministe, s'exécute hors ligne, ne coûte rien et couvre tout de même les chemins qui comptent : le chemin de streaming et la boucle d'outils complète, y compris un tour où un outil est appelé puis où le modèle répond. + +C'est le modèle à conserver lorsque vous étendez l'agent. Tester une intégration LLM contre le modèle réel donne une suite lente, instable et coûteuse, qui ne peut toujours pas vérifier ce que le modèle dira. Tester la *boucle* contre un client scénarisé vérifie exactement ce que vous avez écrit. + +## Étape 9 : Points d'extension + +- **Historique persistant** — implémentez `ConversationStore` au-dessus de Redis ou d'une base de données (étape 7). +- **RAG** — la recherche documentaire est délibérément hors du périmètre du modèle. Le point d'insertion naturel est `ChatAgent.stream` : récupérez le contexte avant la première requête et ajoutez-le en tête du prompt système, ou exposez votre index comme un outil supplémentaire et laissez le modèle décider quand chercher. +- **Authentification et limitation de débit** — tel qu'il est livré, `POST /chat` n'est pas authentifié. Ajoutez une dépendance au routeur avant de l'exposer publiquement ; le [tutoriel Authentification JWT](auth-jwt.md) contient les gardes que vous réutiliseriez. + +## Récapitulatif + +- **Génération** : `fastkit startdemo fastapi-llm-agent`, définir `ANTHROPIC_API_KEY`, `bash scripts/run-server.sh`. +- **Streaming** : SSE avec les événements `token` / `tool_use` / `tool_result` / `usage` / `done` / `error`. +- **Boucle** : flux → appels d'outils → résultats renvoyés dans un seul message utilisateur → répétition, bornée par `MAX_TOOL_ITERATIONS`. +- **Outils** : un schéma dans `TOOL_SCHEMAS` et un gestionnaire dans `TOOL_HANDLERS` ; traitez l'entrée des outils comme non fiable. +- **Mémoire** : interface `ConversationStore`, implémentation par dictionnaire, à remplacer dans `get_store()`. +- **Tests** : un faux client scénarisé, de sorte que la suite est hors ligne et déterministe. + +## Pour aller plus loin + +- [Intégration MCP](mcp-integration.md) — si vous avez besoin d'un serveur MCP plutôt que d'un agent de chat. +- [Authentification JWT](auth-jwt.md) — placer des comptes devant le point d'extrémité de chat. +- [Quel starter choisir ?](../user-guide/choosing-a-starter.md) diff --git a/docs/fr/tutorial/mcp-integration.md b/docs/fr/tutorial/mcp-integration.md index a93ddd7..05f814c 100644 --- a/docs/fr/tutorial/mcp-integration.md +++ b/docs/fr/tutorial/mcp-integration.md @@ -68,8 +68,8 @@ Deploying FastAPI project using 'fastapi-mcp' template │ Dependency 1 │ fastapi │ │ Dependency 2 │ uvicorn │ │ Dependency 3 │ pydantic │ -│ Dependency 4 │ python-jose │ -│ Dependency 5 │ passlib │ +│ Dependency 4 │ PyJWT │ +│ Dependency 5 │ pwdlib[argon2] │ │ Dependency 6 │ python-multipart│ │ Dependency 7 │ mcp │ └──────────────┴────────────────┘ @@ -131,21 +131,21 @@ ai-integrated-api/ ```python from datetime import datetime, timedelta from typing import Optional, Dict, Any -from jose import JWTError, jwt -from passlib.context import CryptContext +import jwt +from pwdlib import PasswordHash from src.core.config import settings -# Password hashing -pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") +# Password hashing — argon2id, the recommended default for new applications. +password_hash = PasswordHash.recommended() def verify_password(plain_password: str, hashed_password: str) -> bool: """Password verification""" - return pwd_context.verify(plain_password, hashed_password) + return password_hash.verify(plain_password, hashed_password) def get_password_hash(password: str) -> str: """Password hashing""" - return pwd_context.hash(password) + return password_hash.hash(password) def create_access_token(data: Dict[str, Any], expires_delta: Optional[timedelta] = None) -> str: """Access token generation""" @@ -189,7 +189,7 @@ def decode_token(token: str) -> Optional[Dict[str, Any]]: algorithms=[settings.ALGORITHM] ) return payload - except JWTError: + except jwt.PyJWTError: return None def verify_token(token: str, token_type: str = "access") -> Optional[str]: @@ -452,8 +452,7 @@ user_db = UserDatabase() from typing import Optional, List from fastapi import Depends, HTTPException, status, Security from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials, APIKeyHeader -from jose import JWTError - +import jwt from src.auth.jwt_handler import decode_token, token_manager from src.auth.models import User, UserInDB, Permission, user_db @@ -486,7 +485,7 @@ async def get_current_user( if user_id is None: raise credentials_exception - except JWTError: + except jwt.PyJWTError: raise credentials_exception user = user_db.get_user_by_id(user_id) diff --git a/docs/fr/tutorial/sqlmodel.md b/docs/fr/tutorial/sqlmodel.md new file mode 100644 index 0000000..5bbba6b --- /dev/null +++ b/docs/fr/tutorial/sqlmodel.md @@ -0,0 +1,260 @@ +# Persistance asynchrone avec `fastapi-sqlmodel` + +Construisez une API adossée à une base de données dans laquelle chaque concept n'est défini **qu'une seule fois**. Les classes SQLModel sont à la fois la table SQLAlchemy et le schéma pydantic : ajouter une colonne ne signifie donc pas modifier quatre fichiers. Ce tutoriel parcourt le modèle `fastapi-sqlmodel` : la hiérarchie des modèles, la base CRUD générique, les réponses paginées, les migrations Alembic asynchrones et la façon d'ajouter votre propre domaine. + +## Ce que vous apprendrez + +- Générer un projet avec `fastkit startdemo fastapi-sqlmodel` +- Comment `ItemBase` → `Item` / `ItemCreate` / `ItemUpdate` / `ItemPublic` élimine la duplication des schémas +- Ce que `CRUDBase` vous apporte gratuitement et où l'étendre +- L'enveloppe de liste `Page[T]` et l'endroit où ses limites sont appliquées +- Exécuter des migrations Alembic asynchrones et passer de SQLite à PostgreSQL + +## Prérequis + +- Python 3.12+ +- FastAPI-fastkit installé (`pip install fastapi-fastkit`) +- Être familier avec `async`/`await` et les notions SQL de base + +Si vous préférez la pile PostgreSQL synchrone basée sur Compose, consultez [Intégration de base de données](database-integration.md), qui utilise `fastapi-psql-orm`. + +## Étape 1 : Générer et lancer + +```console +$ fastkit startdemo fastapi-sqlmodel +Enter the project name: catalog-api +Enter the author name: Developer Kim +Enter the author email: developer@example.com +Enter the project description: Product catalog service +Select package manager (pip, uv, pdm, poetry) [uv]: uv +Do you want to proceed with project creation? [y/N]: y +``` + +```console +$ cd catalog-api +$ bash scripts/migrate.sh # alembic upgrade head +$ bash scripts/run-server.sh # or: uvicorn src.app.main:app --reload +``` + +La `DATABASE_URL` par défaut est `sqlite+aiosqlite:///./app.db` : il n'y a donc aucun serveur de base de données à installer. La documentation est disponible à . + +## Étape 2 : L'arborescence générée + +``` +catalog-api/ +├── pyproject.toml # métadonnées PEP 621 + [tool.fastapi-fastkit] +├── requirements.txt +├── alembic.ini +├── Dockerfile +├── .env +├── scripts/ +│ ├── format.sh lint.sh migrate.sh run-server.sh test.sh +├── src/ +│ └── app/ +│ ├── main.py # application FastAPI + lifespan +│ ├── core/config.py # pydantic-settings +│ ├── db/ +│ │ ├── session.py # moteur asynchrone, dépendance get_session +│ │ └── base.py # agrégation des métadonnées pour Alembic +│ ├── crud/ +│ │ ├── base.py # CRUDBase générique +│ │ └── pagination.py # enveloppe Page[T] +│ ├── alembic/ +│ │ ├── env.py # environnement de migration asynchrone +│ │ └── versions/0001_create_items_table.py +│ ├── api/ +│ │ ├── router.py # regroupe la santé et les routeurs de domaine +│ │ └── health.py # GET /health +│ └── domains/items/ # le domaine d'exemple +│ ├── models.py # table Item + schémas Create/Update/Public +│ ├── crud.py # ItemCRUD(CRUDBase) +│ └── router.py # routeur FastAPI +└── tests/ + ├── conftest.py test_health.py test_items.py test_crud_base.py +``` + +Remarquez ce qu'un dossier de domaine ne contient *pas* : aucun `schemas.py` distinct de `models.py`, et aucun code répétitif de dépôt (repository) propre au domaine. Le premier disparaît dans SQLModel, le second dans `CRUDBase`. + +## Étape 3 : Une seule définition de modèle par concept + +Ouvrez `src/app/domains/items/models.py`. Tout dérive d'une base unique : + +```python +class ItemBase(SQLModel): + """Fields shared by the table and every request/response schema.""" + name: str = Field(min_length=1, max_length=128, index=True) + description: str | None = Field(default=None, max_length=512) + price: float = Field(ge=0) + in_stock: bool = True + + +class Item(ItemBase, table=True): # the actual table + __tablename__ = "items" + id: int | None = Field(default=None, primary_key=True) + created_at: datetime = Field(default_factory=_utcnow) + + +class ItemCreate(ItemBase): ... # POST body — no server-assigned fields + + +class ItemUpdate(SQLModel): # PATCH body — every field optional + name: str | None = Field(default=None, min_length=1, max_length=128) + ... + + +class ItemPublic(ItemBase): # response — base + server-assigned + id: int + created_at: datetime +``` + +Quatre classes, quatre rôles distincts, un seul endroit où vivent les champs : + +| Classe | Rôle | Pourquoi elle est séparée | +|--------------|------------------------------------|---------------------------| +| `ItemBase` | définitions de champs partagées | l'unique source de vérité | +| `Item` | la table de la base de données | ajoute `id`, `created_at` | +| `ItemCreate` | corps de requête pour `POST` | ne doit pas accepter `id` | +| `ItemUpdate` | corps de requête pour `PATCH` | chaque champ est facultatif | +| `ItemPublic` | corps de réponse | expose les champs attribués par le serveur | + +**Le bénéfice :** ajouter une colonne `sku` revient à ajouter une ligne dans `ItemBase`. La table reçoit la colonne, le corps de création l'accepte et la réponse la renvoie — sans aucune autre modification. `ItemUpdate` reste volontairement explicite : la sémantique de mise à jour partielle est une décision, pas une dérivation. + +## Étape 4 : Un CRUD que vous ne réécrivez pas + +`src/app/crud/base.py` est une classe générique paramétrée par le modèle de table et ses deux schémas d'entrée : + +```python +class CRUDBase(Generic[ModelType, CreateSchemaType, UpdateSchemaType]): + async def get(self, session, obj_id) -> ModelType | None: ... + async def list(self, session, *, offset=0, limit=20) -> Sequence[ModelType]: ... + async def count(self, session) -> int: ... + async def create(self, session, obj_in) -> ModelType: ... + async def update(self, session, db_obj, obj_in) -> ModelType: ... + async def delete(self, session, db_obj) -> None: ... +``` + +Un détail mérite d'être bien assimilé — `update` utilise `exclude_unset=True` : + +```python +changes: dict[str, Any] = obj_in.model_dump(exclude_unset=True) +``` + +C'est ce qui rend `PATCH` réellement partiel. Un champ que le client n'a pas envoyé est absent du dictionnaire et n'est donc jamais écrit ; sans `exclude_unset`, un `PATCH` écraserait silencieusement les colonnes non modifiées avec les valeurs par défaut du schéma. + +Un domaine hérite de la base et n'ajoute que ce qui lui est propre : + +```python +class ItemCRUD(CRUDBase[Item, ItemCreate, ItemUpdate]): + """CRUD for Item, extended with a name search.""" + + async def search_by_name(self, session, term, *, limit=20): + statement = select(Item).where(Item.name.like(f"%{term}%")).limit(limit) + result = await session.exec(statement) + return result.all() + + +item_crud = ItemCRUD(Item) +``` + +## Étape 5 : Listes paginées + +Les points d'extrémité de liste renvoient une enveloppe `Page[T]` plutôt qu'un simple tableau : + +```json +{"items": [...], "total": 42, "offset": 0, "limit": 20} +``` + +Le plafonnement se fait dans le routeur, et non côté client : + +```python +limit: int = Query(default=settings.DEFAULT_PAGE_LIMIT, ge=1) +... +limit = min(limit, settings.MAX_PAGE_LIMIT) +``` + +Ainsi, `?limit=100000` ne peut pas transformer une requête en parcours complet de la table. `Page` expose aussi une propriété `has_next` dérivée de `offset`, `len(items)` et `total`, ce qui correspond généralement à ce dont un front-end a réellement besoin. + +Les points d'extrémité fournis par le domaine items : + +| Méthode | Point d'extrémité | Description | +|---------|---------------------------|--------------------------------------| +| GET | `/api/v1/health` | Sonde de vivacité | +| GET | `/api/v1/items` | Lister les éléments (`offset`, `limit`) | +| GET | `/api/v1/items/{item_id}` | Lire un élément | +| POST | `/api/v1/items` | Créer un élément (`201`) | +| PATCH | `/api/v1/items/{item_id}` | Mettre à jour partiellement un élément | +| DELETE | `/api/v1/items/{item_id}` | Supprimer un élément (`204`) | + +Essayez : + +```console +$ curl -X POST http://127.0.0.1:8000/api/v1/items \ + -H 'Content-Type: application/json' \ + -d '{"name": "Keyboard", "price": 89.0}' + +$ curl 'http://127.0.0.1:8000/api/v1/items?offset=0&limit=5' +``` + +## Étape 6 : Asynchrone de bout en bout + +`src/app/db/session.py` construit un `AsyncEngine` par processus et distribue des sessions limitées à la requête : + +```python +async def get_session() -> AsyncGenerator[AsyncSession, None]: + async with async_session_factory() as session: + yield session +``` + +Les routeurs en dépendent (`session: AsyncSession = Depends(get_session)`), `CRUDBase` la prend comme premier argument, et chaque appel à la base de données est attendu avec `await`. Rien dans le chemin de la requête ne bloque la boucle d'événements sur des E/S. + +`create_db_and_tables()` se trouve aussi ici et est appelée depuis le `lifespan` de l'application. C'est une commodité pour la valeur SQLite par défaut et pour les tests — une fois qu'Alembic gère le schéma, définissez `CREATE_TABLES_ON_STARTUP=false`. + +## Étape 7 : Ajouter un domaine + +1. Créez `src/app/domains//` sur le modèle de `items` (`models.py`, `crud.py`, `router.py`). +2. **Importez le modèle de table dans `src/app/db/base.py`.** C'est l'étape facile à oublier et déroutante à déboguer : l'autogénération d'Alembic et `create_db_and_tables` ne voient que les modèles qui ont été importés, si bien qu'un import manquant se traduit par une migration vide plutôt que par une erreur. +3. Enregistrez le routeur dans `src/app/api/router.py`. Les ancres `# fastkit:imports` et `# fastkit:routes` marquent l'endroit où `fastkit addroute` insère du code — placez le vôtre aux mêmes endroits. +4. Générez une migration : `alembic revision --autogenerate -m "add "`. +5. Ajoutez `tests/test_.py` sur le modèle de `tests/test_items.py`. + +## Étape 8 : Migrations et PostgreSQL + +L'environnement Alembic situé dans `src/app/alembic/env.py` est asynchrone : il s'exécute donc avec la même pile de pilotes que l'application. Au quotidien : + +```console +$ bash scripts/migrate.sh # upgrade head +$ alembic revision --autogenerate -m "add sku to items" # after a model change +``` + +Changer de base de données se résume à changer d'URL et de pilote : + +```console +$ uv pip install "asyncpg>=0.31.0" +$ export DATABASE_URL="postgresql+asyncpg://user:password@localhost:5432/app" +$ bash scripts/migrate.sh +``` + +Rien ne change dans les modèles, le CRUD ou les routeurs — c'est tout l'intérêt de garder le pilote derrière `create_engine`. + +## Étape 9 : Les tests + +```console +$ bash scripts/test.sh # or: pytest +``` + +Chaque test s'exécute contre une base SQLite en mémoire toute neuve : pas de migrations, pas de fichiers de fixtures, pas de nettoyage entre les tests. `test_crud_base.py` exerce directement la base générique — sa lecture est utile avant de l'étendre, car il documente le comportement de `exclude_unset` et le contrat count/offset dont héritent vos propres domaines. + +## Récapitulatif + +- **Génération** : `fastkit startdemo fastapi-sqlmodel` → `bash scripts/migrate.sh` → `bash scripts/run-server.sh`. +- **Modèles** : `ItemBase` contient les champs ; la table et les schémas d'E/S en dérivent, si bien qu'une nouvelle colonne est déclarée une seule fois. +- **CRUD** : `CRUDBase` fournit get / list / count / create / update / delete ; les domaines en héritent pour leurs propres requêtes. +- **Listes** : enveloppe `Page[T]` avec plafonnement de la limite côté serveur. +- **Schéma** : Alembic asynchrone ; pensez à importer les nouveaux modèles dans `src/app/db/base.py`. +- **Base de données** : SQLite par défaut, PostgreSQL en modifiant `DATABASE_URL` et en installant `asyncpg`. + +## Pour aller plus loin + +- [Authentification JWT](auth-jwt.md) — placez des comptes et des routes protégées devant cette couche de données. +- [FastAPI orienté domaine](domain-starter.md) — la disposition commune aux deux modèles, sans base de données. +- [Quel starter choisir ?](../user-guide/choosing-a-starter.md) diff --git a/docs/fr/user-guide/choosing-a-starter.md b/docs/fr/user-guide/choosing-a-starter.md index 7a0985f..509ec45 100644 --- a/docs/fr/user-guide/choosing-a-starter.md +++ b/docs/fr/user-guide/choosing-a-starter.md @@ -17,6 +17,9 @@ Le reste de cette page explique pourquoi, et quand opter pour autre chose. | Vous voulez le squelette le plus minimal possible | `fastkit init --interactive` (préréglage : **`minimal`**) | | Vous écrivez un prototype rapide ou un script monofichier | `fastkit init --interactive` (préréglage : **`single-module`**) | | Vous avez besoin d'une vraie base de données (PostgreSQL + SQLAlchemy + Alembic) | `fastkit startdemo fastapi-psql-orm` | +| Vous voulez une persistance asynchrone avec migrations et CRUD générique | `fastkit startdemo fastapi-sqlmodel` | +| Vous avez besoin de comptes utilisateurs, de connexion et de routes protégées dès le premier jour | `fastkit startdemo fastapi-auth-jwt` | +| Vous construisez un backend de chat LLM avec streaming et outils | `fastkit startdemo fastapi-llm-agent` | | Vous voulez une disposition orientée domaine pour une API de taille moyenne | `fastkit init --interactive` (préréglage : **`domain-starter`**) | ## `startdemo` vs `init --interactive` — quelle différence ? @@ -112,11 +115,33 @@ Pour le contrat complet par fonctionnalité (chemins cibles de configuration bas | `fastapi-empty` | `minimal` | Squelette nu ; même forme que celle obtenue par `minimal`. | | `fastapi-single-module` | `single-module` | Démo monofichier. | | `fastapi-domain-starter` | `domain-starter` | Valeur par défaut moderne recommandée ; livré avec un exemple de domaine items. | -| `fastapi-async-crud` | `classic-layered` | Équivalent asynchrone de `fastapi-default`. | +| `fastapi-auth-jwt` | `domain-starter` | Votre API a besoin de comptes. Jetons d'accès / de rafraîchissement, hachage argon2id, rôles et portées (scopes), migrations Alembic. | +| `fastapi-sqlmodel` | `domain-starter` | La persistance asynchrone avant tout : SQLModel + SQLAlchemy asynchrone, migrations, une base CRUD générique et des listes paginées. | +| `fastapi-llm-agent` | `domain-starter` | Un backend de chat Claude en streaming, avec une véritable boucle d'appels d'outils et des tests hors ligne. | | `fastapi-custom-response` | `classic-layered` | Illustre les enveloppes / formats de réponses personnalisés. | -| `fastapi-dockerized` | `classic-layered` | Ajoute un Dockerfile prêt pour la production à la disposition par défaut. | -| `fastapi-psql-orm` | (aucun préréglage direct) | PostgreSQL + SQLAlchemy + Alembic. À choisir lorsque vous avez besoin d'une vraie base de données. | +| `fastapi-psql-orm` | (aucun préréglage direct) | PostgreSQL + SQLAlchemy + Alembic, en mode synchrone. À choisir lorsque vous voulez une pile Compose autour d'une vraie base de données. | | `fastapi-mcp` | (aucun préréglage direct) | Intégration du Model Context Protocol. | +| `fastapi-async-crud` | `classic-layered` | ⚠️ **Obsolète.** Équivalent asynchrone de `fastapi-default` ; préférez `fastapi-sqlmodel`. | +| `fastapi-dockerized` | `classic-layered` | ⚠️ **Obsolète.** Ajoute un Dockerfile à la disposition par défaut ; tous les modèles actuels en fournissent un. | + +### Les trois nouveaux starters + +Ajoutés dans v1.4.0, ces trois modèles suivent tous la forme de `domain-starter` (`src/app/` avec un dossier par concept) : ils s'articulent donc avec ce que vous connaissez déjà de ce préréglage. + +**`fastapi-auth-jwt` — une authentification qui résiste à la mise en production.** Les jetons d'accès ont une durée de vie courte ; les jetons de rafraîchissement sont renouvelés par rotation à chaque utilisation et suivis par leur `jti` en base de données, de sorte qu'un jeton rejoué est rejeté au lieu d'être accepté silencieusement. Les mots de passe sont hachés avec argon2id via `pwdlib`. La déconnexion est réelle : révoquez une session ou toutes les sessions d'un utilisateur. Les gardes sont fournies sous forme de dépendances — `get_current_user`, `get_current_active_superuser` et une fabrique `require_scopes(...)`. Le modèle fonctionne sur SQLite sans aucun service externe et passe à PostgreSQL en définissant `DATABASE_URL`. Tutoriel : [Authentification JWT](../tutorial/auth-jwt.md). + +**`fastapi-sqlmodel` — une seule définition de modèle par concept.** Une classe SQLModel est à la fois la table SQLAlchemy et le schéma pydantic : une nouvelle colonne est donc déclarée une seule fois, et chaque schéma de requête / réponse en hérite. Asynchrone de bout en bout, migrations Alembic asynchrones, un `CRUDBase` qui fournit create / read / list / count / update / delete afin que chaque domaine n'écrive que ce qui lui est propre, et des points d'extrémité de liste enveloppés dans une enveloppe de page. Tutoriel : [Persistance asynchrone avec SQLModel](../tutorial/sqlmodel.md). + +**`fastapi-llm-agent` — un backend de chat, pas un simple wrapper `POST /completion`.** Les jetons sont diffusés au client via SSE, et une boucle d'appels d'outils s'exécute jusqu'à ce que le modèle ait terminé ou que le plafond d'itérations soit atteint. L'historique des conversations se trouve derrière une interface `ConversationStore` : passer du dictionnaire fourni à Redis ne demande donc qu'une implémentation. La suite de tests remplace le client Anthropic par une imitation scénarisée — déterministe, hors ligne, sans clé d'API. Tutoriel : [Agent LLM en streaming](../tutorial/llm-agent.md). + +### À propos des modèles obsolètes + +`fastapi-async-crud` et `fastapi-dockerized` sont toujours livrés et génèrent toujours des projets fonctionnels — rien ne casse si vous les utilisez. Ils sont marqués obsolètes parce que la raison de les choisir a disparu : + +- `fastapi-dockerized` existait pour ajouter un Dockerfile. Tous les modèles actuels en fournissent un, et la sélection interactive du déploiement génère les fichiers Docker pour n'importe quel préréglage. +- `fastapi-async-crud` illustrait des points d'extrémité asynchrones au-dessus d'un magasin factice. `fastapi-sqlmodel` fait la même chose avec une vraie base de données asynchrone, migrations comprises. + +Les projets existants générés à partir de l'un ou l'autre de ces modèles ne nécessitent aucune action. `fastkit list-templates` affiche la liste à jour avec une description en une ligne. @@ -141,5 +166,6 @@ La [Matrice des préréglages d'architecture](../reference/preset-feature-matrix - [Démarrage rapide](quick-start.md) — créer concrètement votre premier projet. - [Créer des projets](creating-projects.md) — visite plus approfondie des drapeaux du CLI. +- [Tutoriel Authentification JWT](../tutorial/auth-jwt.md), [tutoriel Persistance asynchrone avec SQLModel](../tutorial/sqlmodel.md) et [tutoriel Agent LLM en streaming](../tutorial/llm-agent.md) — parcours complets des trois starters ajoutés dans v1.4.0. - [Tutoriel Projet orienté domaine](../tutorial/domain-starter.md) — si vous avez choisi `domain-starter`, vous y trouverez un parcours complet de l'arborescence générée, de l'exemple `items` fourni et de la façon d'ajouter votre prochain domaine. - [Matrice des préréglages d'architecture](../reference/preset-feature-matrix.md) — le contrat complet par préréglage / par fonctionnalité. diff --git a/docs/fr/user-guide/cli-reference.md b/docs/fr/user-guide/cli-reference.md index c3943da..bfa647f 100644 --- a/docs/fr/user-guide/cli-reference.md +++ b/docs/fr/user-guide/cli-reference.md @@ -60,9 +60,127 @@ $ fastkit init [OPTIONS] | Option | Description | Défaut | |--------|-------------|---------| +| `--interactive` | Configuration guidée : préréglage d'architecture, puis sélection des fonctionnalités | désactivé | +| `--config ` | Créer le projet à partir d'un fichier de configuration enregistré, sans aucune question | - | +| `--save-config ` | Avec `--interactive`, écrire la configuration répondue à ce chemin | - | +| `--project-name` | Nom du projet (demandé s'il est omis) | - | +| `--author` | Nom de l'auteur (demandé s'il est omis) | - | +| `--author-email` | E-mail de l'auteur (demandé s'il est omis) | - | +| `--description` | Description du projet (demandée si elle est omise) | - | | `--package-manager` | Gestionnaire de paquets à utiliser (pip, uv, pdm, poetry) | uv | +| `--dry-run` | Afficher les fichiers et paquets qui seraient créés, sans rien écrire | désactivé | +| `--no-venv` | Ignorer la création de l'environnement virtuel (implique `--no-install`) | désactivé | +| `--no-install` | Ignorer l'installation des dépendances | désactivé | +| `--yes` / `-y` | Ne pas demander de confirmation avant d'écraser des fichiers existants lors d'un déploiement du projet sur place | désactivé | | `--help` | Afficher l'aide de la commande | - | +#### Fichiers de configuration (`--config` / `--save-config`) + +Les réponses d'une session interactive forment un simple mapping, et ce mapping peut transiter par un fichier. La création de projet devient ainsi quelque chose que vous pouvez committer, relire et rejouer. + +```console +# enregistrer une session interactive +$ fastkit init --interactive --save-config fastkit.config.json + +# la rejouer plus tard, sans aucune question +$ fastkit init --config fastkit.config.json +``` + +Si vous ne passez pas `--save-config`, une exécution interactive menée à son terme demande s'il faut enregistrer les sélections et où les placer. Cette question n'apparaît que lorsqu'une personne est devant le terminal — les exécutions redirigées ou en CI restent silencieuses et s'y abonnent plutôt avec l'option. + +**Les formats pris en charge** sont choisis d'après l'extension du fichier : + +| Extension | Disponibilité | +|---|---| +| `.json` | Toujours (bibliothèque standard) | +| `.toml` | Toujours (bibliothèque standard) | +| `.yaml` / `.yml` | Uniquement lorsque PyYAML est installé — fastkit ne l'ajoute pas comme dépendance d'exécution | + +Un fichier chargé doit contenir un mapping au niveau supérieur avec au moins `project_name`, `author` et `author_email`. Il est ensuite validé avec les mêmes règles que celles des invites interactives (forme du nom de projet, format de l'e-mail, `all_dependencies` doit être une liste), et chaque problème est signalé avant que quoi que ce soit ne soit écrit. Un fichier qui ne liste que des sélections de fonctionnalités — sans `all_dependencies` — est développé par le constructeur interactif, si bien qu'une configuration écrite à la main et une session de l'assistant aboutissent au même ensemble de paquets. + +Un exemple minimal : + +```json +{ + "project_name": "orders-api", + "author": "Developer Kim", + "author_email": "developer@example.com", + "description": "Domain-oriented orders service", + "architecture_preset": "domain-starter", + "package_manager": "uv" +} +``` + +#### Schéma du fichier de configuration + +Chaque fichier de configuration — écrit à la main ou produit par `--save-config` — passe par le même normaliseur avant d'atteindre le générateur (`normalize_project_config` dans `backend/project_builder/config_schema.py`). Une clé inconnue, un choix inconnu pour un axe connu ou une valeur du mauvais type est rejeté avec le nom fautif et la liste des valeurs réellement autorisées, au lieu d'être ignoré silencieusement. + +| Clé | Forme | Remarques | +|---|---|---| +| `project_name`, `author`, `author_email` | chaîne | Requis | +| `description` | chaîne | Requis | +| `architecture_preset` (alias `preset`) | `"minimal"` \| `"single-module"` \| `"classic-layered"` \| `"domain-starter"` | Les deux graphies doivent concorder si elles sont toutes deux présentes | +| `package_manager` | `"pip"` \| `"uv"` \| `"pdm"` \| `"poetry"` | | +| `database` | chaîne, ou `{"type": }` | `PostgreSQL`, `MySQL`, `MongoDB`, `Redis`, `SQLite`, `None` | +| `authentication`, `async_tasks`, `testing`, `caching`, `monitoring`, `migrations`, `logging` | chaîne, ou `{"type": }` | Axes à sélection unique — consultez le [catalogue de fonctionnalités](#catalogue-de-fonctionnalites-du-constructeur-interactif-interactive) ci-dessus pour les choix de chaque axe | +| `utilities`, `tooling` | liste de chaînes | Axes à sélection multiple — même catalogue pour les choix | +| `deployment` | liste de chaînes | Sous-ensemble de `Docker`, `docker-compose` ; choisir `docker-compose` ajoute automatiquement `Docker` | +| `custom_packages` | liste de chaînes | Paquets supplémentaires à installer tels quels | + +Chaque axe accepte soit son choix brut (`"authentication": "JWT"`), soit la forme de mapping `{"type": ...}` qu'utilisent `database` et les propres métadonnées `[tool.fastapi-fastkit]` d'un projet (`"authentication": {"type": "JWT"}`) — les deux se normalisent vers la même forme canonique, et `--save-config` réécrit toujours la forme canonique en chaîne brute. + +Une configuration qui exerce la plupart des axes : + +```json +{ + "project_name": "orders-api", + "author": "Developer Kim", + "author_email": "developer@example.com", + "description": "Domain-oriented orders service", + "architecture_preset": "domain-starter", + "package_manager": "uv", + "database": "PostgreSQL", + "authentication": "JWT", + "async_tasks": "Celery", + "caching": "Redis", + "migrations": "Alembic", + "tooling": ["ruff", "makefile"], + "deployment": ["Docker"] +} +``` + +La même configuration en TOML : + +```toml +project_name = "orders-api" +author = "Developer Kim" +author_email = "developer@example.com" +description = "Domain-oriented orders service" +architecture_preset = "domain-starter" +package_manager = "uv" +database = "PostgreSQL" +authentication = "JWT" +async_tasks = "Celery" +caching = "Redis" +migrations = "Alembic" +tooling = ["ruff", "makefile"] +deployment = ["Docker"] +``` + +#### Prévisualiser et ignorer des étapes + +`--dry-run` affiche l'arborescence qui serait créée et les paquets qui seraient installés — avec le gestionnaire de paquets qui les installerait — puis se termine sans toucher au disque : + +```console +$ fastkit init --config fastkit.config.json --dry-run +``` + +`--no-install` s'arrête après la création de l'environnement virtuel ; `--no-venv` ignore également l'environnement (et donc l'installation). Les deux sont utiles dans les conteneurs, en CI et partout où l'environnement est géré en dehors de fastkit. + +#### Déploiement sur place et confirmation d'écrasement (`--yes` / `-y`) + +Lorsqu'un projet est déployé sur place — dans l'espace de travail courant plutôt que dans un nouveau dossier de projet — fastkit demande « Overwrite these files? » avant de remplacer tout fichier déjà existant. `--yes` (ou sa forme courte `-y`) ignore cette confirmation et procède à l'écrasement. Cela n'affecte pas la question distincte « Do you want to proceed with project creation? », qui reste affichée. Dans un environnement non interactif (stdin n'est pas un TTY, comme en CI ou lors d'une exécution redirigée), la confirmation d'écrasement est ignorée automatiquement, même sans `--yes`. + #### Invites interactives La commande `init` vous demandera : @@ -96,6 +214,27 @@ La commande `init` vous demandera : - `redis` — stockage de données en mémoire - `celery` — file de tâches distribuée +#### Catalogue de fonctionnalités du constructeur interactif (`--interactive`) + +`fastkit init --interactive` parcourt un catalogue différent, organisé par axes, au lieu des piles minimal/standard/full ci-dessus (source de vérité : `FastkitConfig.PACKAGE_CATALOG` dans `core/settings.py`, et `DynamicConfigGenerator.build_artifacts()` pour ce que génère chaque choix). Chaque axe vaut `None` (ignoré) par défaut ; `utilities` et `tooling` sont à sélection multiple. + +Chaque sélection génère désormais du code réel, et pas seulement des paquets installés — consultez la [matrice préréglages d'architecture / fonctionnalités](../reference/preset-feature-matrix.md) pour voir en quoi `minimal` / `single-module` (`main.py` régénéré) et `classic-layered` / `domain-starter` (`main.py` préservé, avertissements de câblage manuel) diffèrent. + +| Axe | Choix | Paquets installés | Fichiers générés | +|---|---|---|---| +| `database` | PostgreSQL, MySQL, MongoDB, Redis, SQLite, None | PostgreSQL : `asyncpg`, `sqlalchemy` · MySQL : `aiomysql`, `sqlalchemy` · MongoDB : `motor` · Redis : `redis[hiredis]` · SQLite : `sqlalchemy`, `aiosqlite` | Un module de configuration de base de données au chemin du préréglage (par ex. `src/config/database.py`) | +| `authentication` | JWT, OAuth2, FastAPI-Users, Session-based, None | JWT : `pyjwt[crypto]`, `pwdlib[argon2]` · OAuth2 : `authlib`, `itsdangerous`, `httpx` · FastAPI-Users : `fastapi-users[sqlalchemy]`, `pyjwt[crypto]`, `pwdlib[argon2]` · Session-based : `itsdangerous` | Un module de configuration d'authentification au chemin du préréglage ; OAuth2 et Session-based ajoutent aussi une configuration de middleware dans `main.py` | +| `async_tasks` | Celery, Dramatiq, None | Celery : `celery[redis]`, `redis[hiredis]` · Dramatiq : `dramatiq[redis]`, `redis[hiredis]` | `/worker.py` (worker d'arrière-plan) + `/features/tasks.py` (routes de tâches) | +| `testing` | Basic, Coverage, Advanced, None | Basic : `pytest`, `pytest-asyncio`, `httpx` · Coverage : + `pytest-cov` · Advanced : + `faker`, `factory-boy` | `pytest.ini` ; Advanced ajoute aussi `tests/factories.py` et `tests/test_factories.py` | +| `caching` | Redis, None | Redis : `redis[hiredis]`, `fastapi-cache2`, `jinja2` | `/features/cache.py` (points d'extrémité mis en cache) | +| `monitoring` | Loguru, OpenTelemetry, Prometheus, None | Loguru : `loguru` · OpenTelemetry : `opentelemetry-api`, `opentelemetry-sdk`, `opentelemetry-instrumentation-fastapi`, `opentelemetry-exporter-otlp-proto-http` · Prometheus : `prometheus-client`, `prometheus-fastapi-instrumentator` | Aucun fichier autonome — câblé directement dans `main.py` (import / lifespan / configuration) | +| `utilities` (sélection multiple) | CORS, Rate-Limiting, Pagination, WebSocket, None | CORS : aucun (intégré à FastAPI) · Rate-Limiting : `slowapi` · Pagination : `fastapi-pagination` · WebSocket : `websockets` | CORS et Rate-Limiting sont câblés directement dans `main.py` ; Pagination ajoute `/features/pagination.py`, WebSocket ajoute `/features/websocket.py` | +| `migrations` | Alembic, None | Alembic : `alembic` | `alembic.ini`, `alembic/env.py`, `alembic/script.py.mako`, une révision de référence `alembic/versions/_initial.py`, `scripts/migrate.sh` — uniquement lorsque l'axe base de données est une base SQL | +| `tooling` (sélection multiple) | ruff, pre-commit, github-actions, devcontainer, makefile, None | ruff : `ruff` · pre-commit : `pre-commit` · github-actions / devcontainer / makefile : aucun | ruff fusionne un bloc `[tool.ruff]` dans `pyproject.toml` (ou écrit `ruff.toml` pour les projets `pip`) ; pre-commit écrit `.pre-commit-config.yaml` ; github-actions écrit `.github/workflows/test.yml` ; devcontainer écrit `.devcontainer/devcontainer.json` ; makefile écrit `Makefile` | +| `logging` | structured, None | Aucun (uniquement `logging` + `json` de la bibliothèque standard) | `/logging_config.py` (journalisation JSON structurée + middleware d'identifiant de requête), câblé dans `main.py` | + +Quelle que soit la sélection, chaque projet généré reçoit les points d'extrémité `/health` et `/ready` dans `main.py`, et `--dry-run` affiche exactement cet ensemble de fichiers avant toute écriture. + #### Exemples
@@ -227,6 +366,26 @@ Crée des points d'extrémité CRUD complets : | `PUT` | `/api/v1/users/{user_id}` | Mettre à jour un utilisateur | | `DELETE` | `/api/v1/users/{user_id}` | Supprimer un utilisateur | +#### Où le code est inséré + +`addroute` a besoin de deux informations sur le projet : où placer les nouveaux imports et où enregistrer le routeur. Les modèles et les points d'entrée générés marquent ces deux emplacements par des commentaires d'ancrage : + +```python +# src/app/api/router.py +from src.app.api import health + +# fastkit:imports + +api_router = APIRouter() +api_router.include_router(health.router) + +# fastkit:routes +``` + +Conservez ces commentaires lorsque vous modifiez le fichier — `addroute` insère directement au-dessus de `# fastkit:imports` et de `# fastkit:routes`. Un projet qui les a perdus (ou qui a été généré avant l'existence des ancres) fonctionne toujours : fastkit se rabat sur une insertion fondée sur l'AST qui repère elle-même le bloc d'imports et les enregistrements de routeurs. Les ancres rendent simplement le résultat prévisible. + +`addroute` lit aussi le bloc `[tool.fastapi-fastkit]` du projet pour localiser le point d'entrée : il fonctionne donc sur des dispositions que la seule analyse du disque classerait mal. + ### `startdemo` Créer un projet FastAPI à partir d'un modèle prêt à l'emploi. @@ -241,9 +400,19 @@ $ fastkit startdemo [OPTIONS] | Option | Description | Défaut | |--------|-------------|---------| +| `--project-name` | Nom du projet (demandé s'il est omis) | - | +| `--author` | Nom de l'auteur (demandé s'il est omis) | - | +| `--author-email` | E-mail de l'auteur (demandé s'il est omis) | - | +| `--description` | Description du projet (demandée si elle est omise) | - | | `--package-manager` | Gestionnaire de paquets à utiliser (pip, uv, pdm, poetry) | uv | +| `--dry-run` | Afficher les fichiers et paquets qui seraient créés, sans rien écrire | désactivé | +| `--no-venv` | Ignorer la création de l'environnement virtuel (implique `--no-install`) | désactivé | +| `--no-install` | Ignorer l'installation des dépendances | désactivé | +| `--yes` / `-y` | Ne pas demander de confirmation avant d'écraser des fichiers existants lors d'un déploiement du projet sur place | désactivé | | `--help` | Afficher l'aide de la commande | - | +Comme `init`, `startdemo` refuse de s'exécuter lorsque le répertoire du projet cible existe déjà (`Error: Project '{name}' already exists.`). `--dry-run` fait exception : comme il n'écrit jamais sur le disque, il peut toujours viser un nom qui possède déjà un répertoire. Consultez [Déploiement sur place et confirmation d'écrasement](#deploiement-sur-place-et-confirmation-decrasement-yes-y) ci-dessus pour savoir ce que fait `--yes` lors d'un déploiement dans l'espace de travail courant. + #### Invites interactives La commande `startdemo` vous demandera : @@ -259,11 +428,19 @@ La commande `startdemo` vous demandera : | Modèle | Description | Fonctionnalités | |----------|-------------|----------| | `fastapi-default` | Projet FastAPI simple | CRUD basique, données factices | -| `fastapi-async-crud` | API asynchrone de gestion d'items | Async/await, performance | +| `fastapi-domain-starter` | Starter orienté domaine | Un dossier par concept métier, `/health` | +| `fastapi-auth-jwt` | Authentification JWT | Rotation accès/rafraîchissement, argon2id, rôles et portées, Alembic | +| `fastapi-sqlmodel` | Persistance asynchrone SQLModel | SQLModel + SQLAlchemy asynchrone, Alembic, CRUD générique, pagination | +| `fastapi-llm-agent` | Agent Claude en streaming | Streaming SSE, boucle d'appels d'outils, mémoire de conversation | | `fastapi-custom-response` | Système de réponses personnalisées | Réponses personnalisées, pagination | -| `fastapi-dockerized` | API FastAPI dockerisée | Docker, prêt pour la production | | `fastapi-psql-orm` | API FastAPI avec PostgreSQL | PostgreSQL, SQLAlchemy, Alembic | +| `fastapi-mcp` | Serveur Model Context Protocol | Intégration MCP | +| `fastapi-single-module` | Bac à sable monofichier | Un seul module, sans frontières de package | | `fastapi-empty` | Projet FastAPI minimal | Configuration minimale | +| `fastapi-async-crud` | *(obsolète)* API asynchrone de gestion d'items | Remplacé par `fastapi-sqlmodel` | +| `fastapi-dockerized` | *(obsolète)* API FastAPI dockerisée | L'outillage Docker est désormais fourni avec les modèles plus récents | + +Les modèles obsolètes génèrent toujours des projets fonctionnels ; ils ne sont simplement plus des points de départ recommandés. Consultez [Quel starter choisir ?](choosing-a-starter.md). #### Exemples @@ -331,9 +508,11 @@ INFO: Uvicorn running on http://127.0.0.1:8000 #### Prérequis - Doit être exécuté depuis un répertoire de projet FastAPI -- Le projet doit avoir `src/main.py` avec une app FastAPI +- Le projet doit exposer une app FastAPI — résolue à partir de `[tool.fastapi-fastkit].app_module` lorsqu'il est présent, sinon découverte en recherchant `main.py` - L'environnement virtuel doit être activé +Si ni le point d'entrée enregistré ni la recherche ne trouvent de module, `runserver` signale qu'il n'a pas trouvé `main.py` et s'arrête. + ### `list-templates` Lister tous les modèles de projet FastAPI disponibles. @@ -372,6 +551,35 @@ $ fastkit list-templates
+## Métadonnées du projet (`[tool.fastapi-fastkit]`) + +Chaque projet généré par fastkit enregistre la façon dont il a été produit, dans son propre `pyproject.toml` : + +```toml +[tool.fastapi-fastkit] +managed = true +version = "1.4.0" +template = "fastapi-domain-starter" +preset = "domain-starter" +package_manager = "uv" +app_module = "src.app.main:app" +features = ["database:PostgreSQL", "authentication:JWT"] +``` + +| Clé | Signification | +|---|---| +| `managed` | Toujours `true` ; marque le projet comme géré par fastkit | +| `version` | La version de fastkit qui a généré le projet | +| `template` | Le modèle `startdemo`, ou le modèle de base du préréglage | +| `preset` | Le préréglage d'architecture de `init --interactive` (omis sinon) | +| `package_manager` | Le gestionnaire utilisé pour construire l'environnement | +| `app_module` | Le point d'entrée uvicorn `module:attr` | +| `features` | Liste plate `":"` des sélections interactives | + +Ce bloc est l'unique source de vérité pour le point d'entrée du projet : `fastkit runserver` y lit `app_module` avant de se rabattre sur l'analyse de l'arborescence, `fastkit addroute` l'utilise pour localiser l'application, et les Dockerfiles générés utilisent la même valeur dans leur `CMD`. + +Le bloc est écrit à la toute fin de la génération — les gestionnaires de paquets réécrivent `pyproject.toml` pendant la résolution des dépendances, il doit donc être apposé après — et une régénération ultérieure le remplace au lieu d'en ajouter une seconde copie. Modifier `app_module` à la main est la manière prise en charge d'indiquer à fastkit une disposition que vous avez vous-même réorganisée. + ## Variables d'environnement FastAPI-fastkit prend en compte ces variables d'environnement : @@ -381,6 +589,14 @@ FastAPI-fastkit prend en compte ces variables d'environnement : | `FASTKIT_CONFIG_DIR` | Répertoire de configuration | `~/.fastkit` | | `FASTKIT_TEMPLATES_DIR` | Répertoire de modèles personnalisés | Modèles intégrés | | `FASTKIT_LOG_LEVEL` | Niveau de journalisation | `INFO` | +| `FASTKIT_SUBPROCESS_TIMEOUT` | Remplace tous les délais d'expiration des sous-processus du gestionnaire de paquets, en secondes | par opération (30 / 120 / 900) | + +Les appels au gestionnaire de paquets sont bornés individuellement afin qu'un processus enfant bloqué ne puisse jamais figer le CLI : 30 secondes pour une vérification de disponibilité, 120 pour la création d'un environnement virtuel, 900 pour l'installation des dépendances. `FASTKIT_SUBPROCESS_TIMEOUT` remplace ces trois valeurs par une seule — utile sur un réseau lent, ou pour échouer rapidement en CI. Une valeur qui n'est pas un entier positif est ignorée. + +```console +$ export FASTKIT_SUBPROCESS_TIMEOUT=1800 +$ fastkit startdemo fastapi-sqlmodel +``` ### Exemples diff --git a/docs/ja/contributing/development-setup.md b/docs/ja/contributing/development-setup.md index 97f557b..d842a0b 100644 --- a/docs/ja/contributing/development-setup.md +++ b/docs/ja/contributing/development-setup.md @@ -212,6 +212,40 @@ Success: no issues found in 12 source files | `make inspect-templates-verbose` | 詳細出力付きで検査 | | `make inspect-template` | 特定のテンプレートを検査 (TEMPLATES パラメータ) | +基盤となるスクリプトには、反復作業中に知っておくと便利なフラグが 3 つあります: +`--offline` (PyPI の鮮度チェックをスキップ)、`--no-smoke` (生成されたプロジェクトを uvicorn で +起動する処理をスキップ — 最も時間のかかるステップです)、`--mypy` (生成されたプロジェクトを型チェック。 +時間がかかるためオプトイン) です。各チェックが実際に何を行うかは +[テンプレート品質保証](../reference/template-quality-assurance.md) を参照してください。 + +```console +$ python scripts/inspect-templates.py --templates fastapi-sqlmodel --offline --no-smoke +``` + +### 生成コードは Jinja2 フラグメントに置かれる + +`fastkit init --interactive` がプロジェクトに書き込むもの — `main.py`、データベースと認証のモジュール、 +Docker ファイル、pytest の設定 — はすべて、Python の文字列から組み立てられるのではなく、 +`src/fastapi_fastkit/fragments/` 配下のテンプレートからレンダリングされます。`main.py.j2` はどの機能も +ハードコードしていません: `DynamicConfigGenerator` から渡される `import_fragments`、`lifespan_fragments`、 +`setup_fragments` の各リストを順に処理してそれぞれを include するため、機能を自由に組み合わせられます。 + +機能を追加する手順はおおよそ次のとおりです: + +1. 各部品を `main/imports/`、`main/lifespan/`、`main/setup/` のいずれか (または複数) に置きます + (lifespan のフラグメントはスペース 4 つでインデントし、他の 2 つはインデントしません)。 +2. 該当する設定の参照で条件分岐させたうえで、`DynamicConfigGenerator._main_fragments()` に登録します。 +3. 単独のモジュールの場合は `/.py.j2` を追加し、`self._render(...)` でレンダリングします。 +4. `tests/test_backends/test_project_builder_config_generator.py` の組み合わせマトリクスを拡張します — + このテストは、レンダリングされたすべての Python ファイルが `ast.parse` でパースできることを検証します。 + +つまずきやすい制約が 2 つあります: すべてのフラグメントは末尾の改行で終わる必要があり +(環境は `keep_trailing_newline=True` で構築されています)、`main.py.j2` は `# fastkit:imports` / +`# fastkit:routes` のアンカーを保持し続ける必要があります — `fastkit addroute` はそこに挿入します。 + +正式な手順はフラグメント自体のすぐ隣、 +[`src/fastapi_fastkit/fragments/README.md`](https://github.com/bnbong/FastAPI-fastkit/blob/main/src/fastapi_fastkit/fragments/README.md) にあります。 + ### ドキュメントコマンド | コマンド | 説明 | @@ -353,13 +387,16 @@ FastAPI-fastkit/ - **`src/fastapi_fastkit/`** — メインパッケージのソース - **`cli.py`** — メイン CLI のエントリポイント - **`backend/`** — コアのバックエンドロジック - - **`inspector.py`** — テンプレートインスペクタ + - **`inspection/`** — テンプレート検査パイプライン (context、checks、consistency、freshness、smoke、lint、report) + - **`inspector.py`** — `inspection/` に対するファサード。従来の import パスのために残されている - **`interactive/`** — 対話型モードの構成要素 (プロンプト、セレクタ、バリデータ) - **`package_managers/`** — パッケージマネージャー実装 (pip、uv、pdm、poetry) - - **`project_builder/`** — プロジェクト構築ユーティリティ + - **`project_builder/`** — プロジェクト構築ユーティリティ (フラグメント駆動の設定ジェネレーターを含む) + - **`scaffolder.py`** — `init` と `startdemo` が共有する生成パイプライン - **`transducer.py`** — テンプレートトランスデューサ - **`core/`** — コアの設定と例外 - - **`fastapi_project_template/`** — プロジェクトテンプレート (fastapi-default、fastapi-async-crud など) + - **`fragments/`** — 生成されるプロジェクトにレンダリングされる Jinja2 フラグメント (前述) + - **`fastapi_project_template/`** — プロジェクトテンプレート (fastapi-default、fastapi-domain-starter など) - **`utils/`** — 共有ユーティリティ - **`tests/`** — テストスイート - **`test_backends/`** — バックエンド固有のテスト diff --git a/docs/ja/contributing/template-creation-guide.md b/docs/ja/contributing/template-creation-guide.md index b39b641..910cc01 100644 --- a/docs/ja/contributing/template-creation-guide.md +++ b/docs/ja/contributing/template-creation-guide.md @@ -97,8 +97,7 @@ fastapi-{template-name}/ │ ├── lint.sh-tpl # リンティング │ ├── run-server.sh-tpl # サーバー起動 │ └── test.sh-tpl # テスト実行 -├── pyproject.toml-tpl # ✅ 一次メタデータ (PEP 621、推奨) -├── setup.py-tpl # 🟡 レガシーメタデータ (互換のため受け付け) +├── pyproject.toml-tpl # ✅ 一次メタデータ (PEP 621、必須) ├── requirements.txt-tpl # 🟡 pyproject が依存関係を宣言する場合は任意 ├── setup.cfg-tpl # 開発ツール設定 ├── README.md-tpl # ✅ プロジェクトドキュメント (必須) @@ -110,10 +109,16 @@ fastapi-{template-name}/ - `tests/` ディレクトリ - `README.md-tpl` -- 少なくとも 1 つのメタデータファイル: `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` (PEP 621) +- `pyproject.toml-tpl` の `[project].dependencies` または `requirements.txt-tpl` に `fastapi` を依存関係として宣言 -`requirements.txt-tpl` は、`pyproject.toml-tpl` が `[project].dependencies` を宣言している場合は厳密には不要です。新しいテンプレートは **`pyproject.toml-tpl` を一次メタデータファイルとして採用すべきです**。 +`requirements.txt-tpl` は、`pyproject.toml-tpl` が `[project].dependencies` を宣言している場合は不要です — ただし、テンプレートが両方を提供する場合は内容が一致している必要があります: 両者の間で依存関係が食い違っていると検査は失敗します。 + +**新しいテンプレートは pyproject を第一とし、`setup.py-tpl` を含めてはいけません。** v1.4.0 時点で、同梱テンプレートに `setup.py-tpl` を持つものはありません。リポジトリ外のテンプレートが引き続き動作するよう検査はこのファイルをまだ受け付けますが、これはレガシーな経路です。 + +**Python 3.12 を一貫してターゲットにする**: `requires-python = ">=3.12"`、black の `target-version = ["py312"]`、mypy の `python_version = "3.12"`、そして Docker のベースイメージには `python:3.12-slim` を使います。検査では 3 つのピン指定が一致しているかを確認します。 + +**生成されるプロジェクトのランタイム依存関係として `FastAPI-fastkit` を宣言してはいけません。** CLI は開発ツールです。テンプレートの依存関係リストには、生成されたアプリケーション自体が import するものだけを含めるべきです。 ### ファイル作成ガイド @@ -238,52 +243,23 @@ isort==5.12.0 mypy==1.7.1 ``` -#### 4. setup.py-tpl の書き方 (レガシー — pyproject がある場合は任意) +#### 4. ルートアンカー -レガシーテンプレートのために残されています。`pyproject.toml-tpl` を提供する新規テンプレートでは、このファイルは不要です。 +`main.py-tpl` や API 集約モジュールを提供するテンプレートでは、`fastkit addroute` がコードを挿入する場所に印を付けておく必要があります: ```python -""" - パッケージのセットアップ +from src.app.api import health -FastAPI-fastkit で作成されたプロジェクトです。 -""" -from setuptools import find_packages, setup - -# 依存関係リスト (型注釈が必要) -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", -] +# fastkit:imports -setup( - name="", - version="1.0.0", - description="[FastAPI-fastkit templated] ", # 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", - ], -) +api_router = APIRouter() +api_router.include_router(health.router) + +# fastkit:routes ``` +アンカーがない場合、`addroute` は AST ベースの挿入にフォールバックし、import ブロックとルーター登録の位置を自分で探します — そのためアンカーのないテンプレートでも動作はしますが、結果の予測可能性は下がります。これらのファイルを編集するときは、コメントをそのまま残してください。 + #### 5. テストファイルの書き方 ```python @@ -343,8 +319,17 @@ make inspect-template TEMPLATES="fastapi-your-template" # 詳細出力付きで検証 python scripts/inspect-templates.py --templates "fastapi-your-template" --verbose + +# 高速なローカルループ: ネットワークなし、サーバー起動なし +python scripts/inspect-templates.py --templates "fastapi-your-template" \ + --offline --no-smoke + +# PR を出す前の完全なチェック (生成されたプロジェクトに対する mypy を追加) +python scripts/inspect-templates.py --templates "fastapi-your-template" --mypy ``` +`--offline` は PyPI での依存関係の鮮度チェックをスキップし、`--no-smoke` は生成されたプロジェクトの起動 (最も時間のかかるステップ) をスキップし、`--mypy` は生成されたプロジェクトを型チェックします。各チェックの内容は [テンプレート品質保証](../reference/template-quality-assurance.md) を参照してください。 + !!! note PR を提出すると、**Template PR Inspection** ワークフローが自動的に走り、テンプレート変更を検証します。フィードバックは PR に直接届きます。 @@ -357,7 +342,7 @@ python scripts/inspect-templates.py --templates "fastapi-your-template" --verbos - [ ] `tests/` ディレクトリが存在 - [ ] `README.md-tpl` ファイルが存在 -- [ ] `pyproject.toml-tpl` (推奨) または `setup.py-tpl` (レガシー) のいずれかが存在 +- [ ] `pyproject.toml-tpl` が存在 (新しいテンプレートには `setup.py-tpl` を含めない) #### ✅ 拡張子の検証 @@ -369,7 +354,8 @@ python scripts/inspect-templates.py --templates "fastapi-your-template" --verbos - [ ] 次のいずれかに `fastapi` が宣言されている: - [ ] `pyproject.toml-tpl` の `[project].dependencies` (推奨) - [ ] `requirements.txt-tpl` - - [ ] `setup.py-tpl` の `install_requires` +- [ ] `requirements.txt-tpl` と `[project].dependencies` が食い違っていない +- [ ] `FastAPI-fastkit` がランタイム依存関係に **含まれていない** #### ✅ FastAPI 実装の検証 @@ -382,6 +368,24 @@ python scripts/inspect-templates.py --templates "fastapi-your-template" --verbos - [ ] 依存関係のインストールに成功 - [ ] すべての pytest テストが通過 +#### ✅ スモークテスト + +- [ ] 生成されたプロジェクトが uvicorn で起動する +- [ ] `/docs` が応答する +- [ ] `/health` が応答する + +uvicorn のターゲットは、生成された `pyproject.toml` の `[tool.fastapi-fastkit].app_module`、次に `template-config.yml` の `app_module`、最後に慣例的な `main.py` の配置場所の順で解決されます。 + +#### ✅ 設定の一貫性 + +- [ ] `requires-python`、black の `target-version`、mypy の `python_version` がすべて Python 3.12 を示している +- [ ] `requirements.txt-tpl` と `[project].dependencies` の間に依存関係の食い違いがない +- [ ] `FastAPI-fastkit` への自己依存がない + +#### ✅ プレースホルダーの残留 + +- [ ] プロジェクト生成後に `` が残っていない + #### ✅ 自動テンプレートテスト FastAPI-fastkit には、すべてのテンプレートで包括的テストを走らせる **自動テンプレートテスト** が含まれます: diff --git a/docs/ja/index.md b/docs/ja/index.md index 7144400..ea182f0 100644 --- a/docs/ja/index.md +++ b/docs/ja/index.md @@ -34,6 +34,8 @@ - **🔍 自動化されたテンプレート品質保証**: 週次の自動テストですべてのテンプレートが動作し最新であることを保証 - **🚀 多彩なプロジェクトテンプレート**: async CRUD、Docker、PostgreSQL など、ユースケース別の事前構成テンプレートを用意 - **📦 複数のパッケージマネージャーに対応**: 好みの Python パッケージマネージャー (pip、uv、pdm、poetry) を選択可能 +- **🧾 再現可能なプロジェクト設定**: 対話型セッションを `--save-config` で保存し、後で `fastkit init --config` で再現 — プロンプトなしで同じプロジェクトを作成 +- **🏷️ 自己記述的なプロジェクト**: 生成されるすべてのプロジェクトが、テンプレート、プリセット、パッケージマネージャー、エントリポイント、選択した機能を `[tool.fastapi-fastkit]` ブロックに記録し、`runserver` と `addroute` がそれを読み取る ## インストール @@ -123,7 +125,7 @@ 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 │ +│ ℹ Injected metadata into pyproject.toml │ ╰──────────────────────────────────────────────────────╯ ╭──────────────────────── Info ────────────────────────╮ │ ℹ Injected metadata into config file │ @@ -174,6 +176,16 @@ Installing dependencies... このコマンドは、Python の仮想環境を含む新しい FastAPI プロジェクトの開発環境を作成します。 +- 主なオプション: + - `--project-name`、`--author`、`--author-email`、`--description` + - `--package-manager` [pip|uv|pdm|poetry] + - `--config `: 保存済みの設定ファイル (`.json` / `.toml`、PyYAML がインストールされていれば `.yaml` も) から、プロンプトなしでプロジェクトを作成 + - `--save-config `: `--interactive` と併用し、回答を後で再利用できるよう記録 + - `--dry-run`: 作成されるファイルとパッケージを表示するだけで、何も書き込まない + - `--no-venv`: 仮想環境の作成をスキップ (`--no-install` を含意) + - `--no-install`: 依存関係のインストールをスキップ + - `--yes` / `-y`: その場にデプロイするとき、「Overwrite these files?」の確認をスキップ + ### 対話型モードでプロジェクトを作成 ✨ NEW! より複雑なプロジェクトには、**対話型モード** を使って、賢い機能選択とともに段階的に FastAPI アプリケーションを組み立てましょう: @@ -264,6 +276,30 @@ Select utilities (comma-separated numbers, e.g., 1,3,4): Select utilities: 1 +🪵 Logging Format +Select logging format: + 1. structured - JSON logs + X-Request-ID correlation middleware (stdlib only) + 2. None - Uvicorn's default plain-text logging + +Select logging format [2]: 1 + +🧬 Database Migrations +Select migration tool: + 1. Alembic - alembic.ini + async alembic/env.py + scripts/migrate.sh (recommended for SQL databases) + 2. None - No migration tooling + +Select migration tool [1]: 1 + +🧰 Developer Tooling +Select tooling to generate (comma-separated numbers, e.g., 1,3) + 1. ruff ruff + ruff-format config in pyproject.toml (replaces black/isort) + 2. pre-commit .pre-commit-config.yaml + 3. github-actions .github/workflows/test.yml (Python 3.12) + 4. devcontainer .devcontainer/devcontainer.json + 5. makefile Makefile with install/test/lint/format/run + +Your choice (or press Enter to skip): 1,2 + 🚀 Deployment Configuration Select deployment option: 1. Docker - Generate Dockerfile @@ -294,6 +330,9 @@ Enter custom package names (comma-separated, press Enter to skip): │ Monitoring │ Prometheus │ │ Testing │ Coverage │ │ Utilities │ CORS │ +│ Logging │ structured │ +│ Migrations │ Alembic │ +│ Tooling │ ruff, pre-commit │ │ Package Manager │ uv │ └─────────────────────┴───────────────────────────────────────────────────────────────────────────┘ @@ -350,6 +389,23 @@ Installing dependencies... - 自動 pip 互換性を備えた **スマートな依存関係管理** - プリセットが自動配線できない選択について手動配線の警告を出力する **機能検証** - 生成された `pyproject.toml` への **識別マーカー** 注入 (description マーカー + `[tool.fastapi-fastkit]` テーブル) — 後で `is_fastkit_project()` が生成済みプロジェクトを認識可能 +- `[tool.fastapi-fastkit]` に記録される **プロジェクトメタデータ** — テンプレート、プリセット、パッケージマネージャー、`app_module`、選択した `features` — を `fastkit runserver` と `fastkit addroute` が読み取る +- **再現可能な実行**: `--save-config ` で回答を記録し、`--config ` でプロンプトを一切出さずに再現でき、`--dry-run` で何も書き込まずに結果をプレビュー可能 + +現在では、カタログのどの選択もパッケージをインストールするだけでなく、実際に動作するコードを生成します — +バックグラウンドタスク (Celery/Dramatiq)、Redis キャッシュ、WebSocket、ページネーション、OpenTelemetry +トレーシング、OAuth2 / セッションベース認証のすべてに、対応するモジュールと `main.py` の配線が付属します。 +さらに 3 つの新しい項目がカタログを補完します: + +- **ロギング**: `structured` は、標準ライブラリの `logging` + `json` による構造化出力と request-id ミドルウェアを追加 — 新しい依存関係は不要 +- **マイグレーション**: `Alembic` は、SQL データベースを選択した場合に完全な非同期マイグレーション環境 (`alembic.ini`、`alembic/env.py`、ベースラインのリビジョン、`scripts/migrate.sh`) を生成 +- **ツール** (複数選択): `ruff`、`pre-commit`、`github-actions`、`devcontainer`、`makefile` — それぞれが自身の設定ファイルを生成し、`ruff` は `pyproject.toml` に `[tool.ruff]` ブロックもマージ + +生成されるすべてのプロジェクトには `/health` と `/ready` エンドポイントも含まれ、`--dry-run` はこれらの新しい +成果物を含め、実際の実行で書き込まれるのとまったく同じファイルセットを一覧表示します。`classic-layered` と +`domain-starter` はテンプレート同梱の `main.py` を保存するため、これらのいずれかの項目の選択に手動配線が必要な +場合は機能検証が警告を出します — 詳しくは [プリセット / 機能マトリクス](reference/preset-feature-matrix.md) を +参照してください。 ### FastAPI プロジェクトに新しいルートを追加 @@ -445,6 +501,12 @@ FastAPI template project will deploy at '~your-project-path~' +- 主なオプション: + - `--project-name`、`--author`、`--author-email`、`--description` + - `--package-manager` [pip|uv|pdm|poetry] + - `--dry-run`、`--no-venv`、`--no-install`、`--yes`/`-y` — `init` と同じ意味 + - 対象のプロジェクトディレクトリがすでに存在する場合は実行を拒否 (`--dry-run` を除く) + 利用可能な FastAPI デモの一覧を表示するには、次のコマンドを実行してください:
@@ -463,18 +525,42 @@ $ fastkit list-templates │ │ PostgreSQL │ │ fastapi-default │ Simple FastAPI Project │ │ fastapi-single-module │ FastAPI Single Module Template │ +│ fastapi-auth-jwt │ FastAPI JWT Authentication │ +│ fastapi-sqlmodel │ FastAPI SQLModel │ +│ fastapi-llm-agent │ FastAPI LLM Agent │ └────────────────────────┴───────────────────────────────────────────────────────┘ ```
+利用可能なテンプレート: + +| テンプレート | 説明 | 備考 | +|---|---|---| +| [`fastapi-default`](https://github.com/bnbong/FastAPI-fastkit/blob/main/src/fastapi_fastkit/fastapi_project_template/fastapi-default/README.md-tpl) | クラシックなレイヤー型レイアウトのシンプルな FastAPI プロジェクト | 最初の選択肢として最適 | +| [`fastapi-empty`](https://github.com/bnbong/FastAPI-fastkit/blob/main/src/fastapi_fastkit/fastapi_project_template/fastapi-empty/README.md-tpl) | 最小構成の FastAPI テンプレート | `minimal` プリセットのベース | +| [`fastapi-single-module`](https://github.com/bnbong/FastAPI-fastkit/blob/main/src/fastapi_fastkit/fastapi_project_template/fastapi-single-module/README.md-tpl) | 単一ファイルの FastAPI アプリ | `single-module` プリセットのベース | +| [`fastapi-domain-starter`](https://github.com/bnbong/FastAPI-fastkit/blob/main/src/fastapi_fastkit/fastapi_project_template/fastapi-domain-starter/README.md-tpl) | 中規模 API 向けの、ドメイン指向で pyproject を第一とするスターター | `domain-starter` プリセットのベース | +| [`fastapi-auth-jwt`](https://github.com/bnbong/FastAPI-fastkit/blob/main/src/fastapi_fastkit/fastapi_project_template/fastapi-auth-jwt/README.md-tpl) | リフレッシュトークンのローテーション、argon2 ハッシュ化、スコープを備えた JWT 認証 | v1.4.0 で追加 | +| [`fastapi-sqlmodel`](https://github.com/bnbong/FastAPI-fastkit/blob/main/src/fastapi_fastkit/fastapi_project_template/fastapi-sqlmodel/README.md-tpl) | 非同期 SQLModel + Alembic マイグレーション + ページネーション付きの汎用 CRUD | v1.4.0 で追加 | +| [`fastapi-llm-agent`](https://github.com/bnbong/FastAPI-fastkit/blob/main/src/fastapi_fastkit/fastapi_project_template/fastapi-llm-agent/README.md-tpl) | ツール呼び出しループを備えたストリーミング Claude チャットエージェント (SSE) | v1.4.0 で追加 | +| [`fastapi-mcp`](https://github.com/bnbong/FastAPI-fastkit/blob/main/src/fastapi_fastkit/fastapi_project_template/fastapi-mcp/README.md-tpl) | MCP サーバーとして公開される FastAPI アプリ | | +| [`fastapi-psql-orm`](https://github.com/bnbong/FastAPI-fastkit/blob/main/src/fastapi_fastkit/fastapi_project_template/fastapi-psql-orm/README.md-tpl) | PostgreSQL、SQLModel、Alembic、docker-compose を使うアイテム管理 API | Docker が必要 | +| [`fastapi-custom-response`](https://github.com/bnbong/FastAPI-fastkit/blob/main/src/fastapi_fastkit/fastapi_project_template/fastapi-custom-response/README.md-tpl) | カスタムレスポンスエンベロープ、エラー処理、ページネーションを備えたアイテム管理 API | | +| [`fastapi-async-crud`](https://github.com/bnbong/FastAPI-fastkit/blob/main/src/fastapi_fastkit/fastapi_project_template/fastapi-async-crud/README.md-tpl) | 非同期アイテム管理 API | **非推奨** — `fastapi-sqlmodel` を使用 | +| [`fastapi-dockerized`](https://github.com/bnbong/FastAPI-fastkit/blob/main/src/fastapi_fastkit/fastapi_project_template/fastapi-dockerized/README.md-tpl) | Docker 化されたアイテム管理 API | **非推奨** — `init --interactive` の Docker オプションと `fastapi-default` を使用 | + +非推奨のテンプレートも引き続き動作しますが、推奨される出発点ではなくなりました。選び方のガイドは [どのスターターを選ぶべき?](user-guide/choosing-a-starter.md) を参照してください。 + ## ドキュメント 包括的なガイドと詳細な使い方は、ドキュメントを参照してください: +- 🧭 **[どのスターターを選ぶべき?](user-guide/choosing-a-starter.md)** - `startdemo` テンプレートと対話型プリセットを選ぶための初心者向けガイド - 📚 **[ユーザーガイド](user-guide/quick-start.md)** - 詳細なインストール / 利用ガイド - 🎯 **[チュートリアル](tutorial/getting-started.md)** - 初心者向けの段階的なチュートリアル - 📖 **[CLI リファレンス](user-guide/cli-reference.md)** - コマンドの完全リファレンス +- 🧱 **[アーキテクチャプリセットマトリクス](reference/preset-feature-matrix.md)** - 対話型生成におけるプリセットごと / 機能ごとの仕様 - 🔍 **[テンプレート品質保証](reference/template-quality-assurance.md)** - 自動化されたテストと品質基準 ## 🚀 テンプレート別チュートリアル @@ -486,16 +572,19 @@ $ fastkit list-templates - **[基本 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 を構築 +- **[JWT 認証](tutorial/auth-jwt.md)** - `fastapi-auth-jwt` テンプレートで本物のアカウント機能を追加: ローテーションするリフレッシュトークン、argon2id ハッシュ化、ロールとスコープのガード ### 🗄️ データベースとインフラ - **[データベース統合](tutorial/database-integration.md)** - `fastapi-psql-orm` テンプレートで PostgreSQL + SQLAlchemy を活用 -- **[Docker でのデプロイ](tutorial/docker-deployment.md)** - `fastapi-dockerized` テンプレートで本番デプロイ環境を構築 +- **[SQLModel による非同期永続化](tutorial/sqlmodel.md)** - `fastapi-sqlmodel` テンプレートで非同期 SQLModel、Alembic マイグレーション、汎用 CRUD ベース、ページネーションされたリストを活用 +- **[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 サーバーを作成 +- **[ストリーミング LLM エージェント](tutorial/llm-agent.md)** - `fastapi-llm-agent` テンプレートで、本物のツール呼び出しループを備えたストリーミング Claude チャットバックエンドを構築 各チュートリアルが提供するもの: diff --git a/docs/ja/reference/preset-feature-matrix.md b/docs/ja/reference/preset-feature-matrix.md index 1747cbe..ab3cf7c 100644 --- a/docs/ja/reference/preset-feature-matrix.md +++ b/docs/ja/reference/preset-feature-matrix.md @@ -29,12 +29,18 @@ | 機能 | `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` も import されます。 | プリセットのパスに認証設定モジュールを生成します。`main.py` への import は追加されません — 依存性は手動で配線してください。 | -| **バックグラウンドタスク** (Celery、Dramatiq) | パッケージはインストールされますが、現状は main.py オーバーレイなし。 | 同上。 | -| **キャッシュ** (Redis) | パッケージはインストールされますが、現状は main.py オーバーレイなし。 | 同上。 | +| **認証** (JWT、FastAPI-Users、OAuth2、セッションベース) | 認証設定モジュールを生成します。JWT は `HTTPBearer` を import し、OAuth2 とセッションベースは `main.py` にミドルウェアのセットアップも追加します。 | プリセットのパスに認証設定モジュールを生成します。`main.py` への import は追加されません — 依存性は手動で配線してください。 | +| **バックグラウンドタスク** (Celery、Dramatiq) | `worker.py` と `features/tasks.py` を生成し、再生成された `main.py` にタスクルーターをマウントします。 | `worker.py` と `features/tasks.py` を生成しますが、テンプレートに含まれる `main.py` は **そのまま残る** ため、ルーターは手動でマウントしてください。 | +| **キャッシュ** (Redis) | `features/cache.py` を生成し、再生成された `main.py` でマウントと初期化を行います。 | `features/cache.py` を生成します。マウントは手動で行ってください。 | +| **ユーティリティ: ページネーション / WebSocket** | `features/pagination.py` / `features/websocket.py` を生成し、再生成された `main.py` にルーターをマウントします。 | 同じモジュールを生成します。マウントは手動で行ってください。 | +| **モニタリング: OpenTelemetry** | 独立したファイルはありません — トレーシングのセットアップは再生成された `main.py` に直接追加されます。 | 独立したファイルはありません。テンプレートに含まれる `main.py` はそのまま残るため、トレーシングは **配線されません** — セットアップを手動で追加してください。 | +| **マイグレーション** (Alembic) | SQL データベースを選択した場合は常に `alembic.ini`、`alembic/env.py`、ベースラインのリビジョン、`scripts/migrate.sh` を生成します — `main.py` オーバーレイとは独立しています。 | 同上 — Alembic の生成は `main.py` がそのまま残るかどうかに依存しません。 | +| **ツール** (ruff、pre-commit、github-actions、devcontainer、makefile) | 対応する設定ファイルを生成します。`main.py` オーバーレイとは独立しています。 | 同上。 | +| **ロギング** (構造化) | `logging_config.py` を生成し、再生成された `main.py` に request-id ミドルウェアを配線します。 | `logging_config.py` を生成します。テンプレートに含まれる `main.py` はそのまま残るため、ミドルウェアは手動で配線してください。 | | **CORS** (ユーティリティ) | 再生成された `main.py` に `CORSMiddleware` が `allow_origins=['*']` で追加されます。 | テンプレートに含まれる `main.py` に **すでに組み込み済み** です (`settings.all_cors_origins` に応じて条件分岐)。`.env` の `BACKEND_CORS_ORIGINS` を設定すれば有効化されます — コード変更は不要です。 | -| **テスト** (Basic / Coverage / Advanced) | プロジェクトルートに `pytest.ini` を生成。 | 同上。 | +| **テスト** (Basic / Coverage / Advanced) | プロジェクトルートに `pytest.ini` を生成。Advanced では `tests/factories.py` と `tests/test_factories.py` も追加されます。 | 同上。 | | **デプロイ** (Docker、docker-compose) | プロジェクトルートに `Dockerfile` または `docker-compose.yml` を作成。 | 同上。 | +| **`/health` / `/ready`** | 再生成された `main.py` に常に含まれます。 | テンプレートに含まれ、そのまま残る `main.py` にすでに含まれています。 | ## "Preset compatibility" 警告が出る場面 @@ -44,10 +50,15 @@ |---|---| | `CORS` (ユーティリティ) | ❌ — テンプレートに含まれる `main.py` で配線済み。`.env` の `BACKEND_CORS_ORIGINS` を埋めるだけ。 | | `Rate-Limiting` (ユーティリティ) | ✅ — `slowapi` リミッタのセットアップは追加されない | -| `Prometheus` (モニタリング) | ✅ — `Instrumentator().instrument(app)` は呼び出されない | +| `Pagination` / `WebSocket` (ユーティリティ) | ✅ — 生成されたルーター (`features/pagination.py` / `features/websocket.py`) はマウントされない | +| `Celery` / `Dramatiq` (async_tasks) | ✅ — 生成された `worker.py` / `features/tasks.py` のルーターはマウントされない | +| `Redis` (キャッシュ) | ✅ — 生成された `features/cache.py` はマウントも初期化もされない | +| `Prometheus` / `OpenTelemetry` (モニタリング) | ✅ — 計装のセットアップは `main.py` に追加されない | +| `structured` (ロギング) | ✅ — `logging_config.py` は生成されるが、request-id ミドルウェアは `main.py` に追加されない | +| `Alembic` (マイグレーション)、`tooling` の選択 | ❌ — これらのファイルはプリセットに関係なく生成され、そのまま使える。`main.py` に依存するものはない | | 任意のデータベース / 認証選択 | ⚠️ — 設定ファイルは生成されますが、自分で `Depends()` をルーターに組み込む必要があります | -`minimal` と `single-module` プリセットでは、動的 `main.py` オーバーレイが CORS、レート制限、Prometheus 計装を自動で扱います。警告は出ません。 +`minimal` と `single-module` プリセットでは、動的 `main.py` オーバーレイが上記のすべての項目を自動で扱います。警告は出ません。 ## サポート外の組み合わせ (安全側に倒す) diff --git a/docs/ja/reference/template-quality-assurance.md b/docs/ja/reference/template-quality-assurance.md index b156947..3a256c2 100644 --- a/docs/ja/reference/template-quality-assurance.md +++ b/docs/ja/reference/template-quality-assurance.md @@ -21,6 +21,57 @@ FastAPI-fastkit は **2 つの相補的な品質保証システム** を採用 - ✅ **依存関係チェック** — FastAPI と必要な依存関係が正しく宣言されていることを確認 - ✅ **FastAPI 実装の検証** — テンプレートに適切な FastAPI アプリ初期化が含まれていることを検証 - ✅ **テスト実行** — テンプレートに含まれるテストを実行して機能を確認 +- ✅ **スモークテスト** — 生成されたプロジェクトを uvicorn で起動し、HTTP の外部インターフェースを確認 +- ✅ **設定の一貫性** — Python バージョンのピン指定、依存関係の食い違い、自己依存 +- ✅ **プレースホルダーの残留** — プロジェクト生成後に `` が残っていない +- ✅ **依存関係の鮮度** — PyPI の最新リリースより遅れているピン指定について警告 + +## 各チェックの内容 + +検査はテンプレートから **実際のプロジェクト** を生成し — コピー、`-tpl` の変換、メタデータの注入 — +テンプレートのソースではなく、生成されたそのプロジェクトに対してすべてのチェックを実行します。 +テンプレートを読むだけのチェックでは、生成時に置換されなかったプレースホルダーを検出できないからです。 + +### スモークテスト + +生成されたプロジェクトを空いているポートで uvicorn により起動し、インスペクタはサーバーが応答するか +タイムアウトになるまでポーリングします。起動したら `/docs` と `/health` にリクエストを送り、ステータス +コードを確認します。その後サーバープロセスを終了させ、停止しない場合は `SIGKILL` まで段階的に強めます。 + +uvicorn のターゲットは次の順で解決されます: 生成された `pyproject.toml` の `[tool.fastapi-fastkit].app_module`、 +次にテンプレートの `template-config.yml` の `app_module`、最後に慣例的な `main.py` の配置場所です。 +これは `fastkit runserver` が使うものと同じ契約なので、スモークテストで起動できるテンプレートは、 +生成されたプロジェクトも実行できるテンプレートだということになります。 + +このチェックは、静的解析では検出できないものを捕まえます: 実行時にしか解決されない import、例外を送出する +`lifespan`、デフォルト値のない設定フィールドなどです。 + +### 設定の一貫性 + +テンプレートの `pyproject.toml-tpl` に対して 3 つの点を検証します: + +- **Python バージョンのピン指定が一致し、3.12 をターゲットにしている** — + `requires-python`、black の `target-version`、mypy の `python_version`。 + `>=3.12` を宣言しながら `py311` でフォーマットするテンプレートは、いずれコントリビューターを困らせる + バグです。 +- **依存関係の食い違いがない** — `requirements.txt-tpl` と `[project].dependencies` で宣言されている + パッケージが食い違ってはいけません。テンプレートはパッケージマネージャーとの互換性のために両方のファイルを + 提供しますが、何かがチェックしない限り、2 つのリストは必ず乖離していきます。 +- **自己依存がない** — 生成されたプロジェクトは `FastAPI-fastkit` をランタイム依存関係として宣言しては + いけません。CLI は開発ツールです。生成されるすべてのアプリにそれを含めると、無関係なプロジェクトに + fastkit の依存関係ツリー全体が持ち込まれます。 + +### プレースホルダーの残留 + +``、`` などはすべて、生成時に置換されなければなりません。生成されたプロジェクトに +`` が残っている場合はエラーになります — これはメタデータ注入がそのファイルを把握していない +ことを意味し、ユーザーはそれをそのまま受け取ってしまいます。 + +### 依存関係の鮮度 + +ピン指定されたバージョンは PyPI 上の最新リリースと比較され、エラーではなく警告として報告されます — +マイナーバージョンが 1 つ遅れているテンプレートは失敗ではなく、情報です。これはネットワークにアクセスする +唯一のチェックで、`--offline` でスキップできます。 ## 自動テンプレートテストシステム @@ -140,6 +191,30 @@ $ python scripts/inspect-templates.py --verbose $ python scripts/inspect-templates.py --output my_results.json ``` +#### 検査フラグ + +| フラグ | 効果 | 使う場面 | +|---|---|---| +| `--offline` | すべてのネットワーク参照 (依存関係の鮮度レポート) をスキップ | ネットワークがない、または高速で閉じた環境で実行したい | +| `--no-smoke` | 生成されたプロジェクトの uvicorn での起動をスキップ | 構造だけを繰り返し調整している。スモークテストは最も時間のかかるステップ | +| `--mypy` | 生成されたプロジェクトを mypy で型チェック | テンプレートを提出する前 — 時間がかかるためオプトイン | +| `--templates` | 検査するテンプレートのカンマ区切りリスト | 1 つのテンプレートに取り組んでいる | +| `--verbose` | 詳細な出力 | 失敗をデバッグしている | +| `--output` | JSON 結果ファイルのパス | 複数回の実行結果を並べて残したい | + +1 つのテンプレートに対する素早いローカルループ: + +```console +$ python scripts/inspect-templates.py \ + --templates fastapi-sqlmodel --offline --no-smoke --verbose +``` + +...そして PR を出す前の完全なチェック: + +```console +$ python scripts/inspect-templates.py --templates fastapi-sqlmodel --mypy +``` + ### Makefile コマンドを使う ```console @@ -192,10 +267,16 @@ $ make inspect-template TEMPLATES="fastapi-default,fastapi-async-crud" - Python ソースファイルを含む `src/` ディレクトリが存在すること - Python ファイルは `.py-tpl` 拡張子を使用すること - `tests/` ディレクトリと `README.md-tpl` ファイルを含むこと -- 次のメタデータファイルのうち **少なくとも 1 つ** を含むこと: - - `pyproject.toml-tpl` (推奨、PEP 621)、または - - `setup.py-tpl` (レガシー、引き続き受け付け) +- `pyproject.toml-tpl` (PEP 621) を含むこと。同梱テンプレートは pyproject を第一とし、もう `setup.py-tpl` を + 提供していません。サードパーティのテンプレートが引き続き動作するよう検査はこのファイルをまだ受け付けますが、 + 新しいテンプレートで追加してはいけません。 - `requirements.txt-tpl` は、`pyproject.toml-tpl` が `[project].dependencies` を宣言している場合は任意です + — ただし両方が存在する場合は一致している必要があります (*設定の一貫性* を参照) +- Python 3.12 を一貫してターゲットにすること: `requires-python = ">=3.12"`、black の + `target-version = ["py312"]`、mypy の `python_version = "3.12"`、そして Docker のベースイメージには + `python:3.12-slim` +- `FastAPI-fastkit` をランタイム依存関係として宣言しないこと +- 生成後に置換されていない `` を残さないこと ### FastAPI 要件 - FastAPI アプリの初期化を含むこと diff --git a/docs/ja/reference/translation-status.md b/docs/ja/reference/translation-status.md index 279ada4..a29cdff 100644 --- a/docs/ja/reference/translation-status.md +++ b/docs/ja/reference/translation-status.md @@ -18,15 +18,15 @@ FastAPI-fastkit のドキュメントは複数の言語でビルドされます | ロケール | ステータス | Markdown ページ | 備考 | |---|---|---:|---| -| 🇬🇧 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`) | ✅ 完了 | 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` を意図的に再利用しています。 | +| 🇬🇧 English (`en`) | ✅ 原典 (Source of truth) | 29 / 29 | すべての基準になる原文です。 | +| 🇰🇷 Korean (`ko`) | ✅ 完了 | 29 / 29 | Phase 1〜3 は完了しています。2026-10-11 に v1.4.1 の英語原典と再同期しました: v1.4.0 で追加された 3 つのチュートリアル (`tutorial/auth-jwt.md`、`tutorial/sqlmodel.md`、`tutorial/llm-agent.md`) を翻訳し、最初の翻訳以降に英語原典とずれていたページも元どおりに揃えました。`docs/ko/changelog.md` は英語の `CHANGELOG.md` を意図的に再利用しています。 | +| 🇯🇵 Japanese (`ja`) | ✅ 完了 | 29 / 29 | Phase 1〜3 は完了しています。2026-10-11 に v1.4.1 の英語原典と再同期しました: v1.4.0 で追加された 3 つのチュートリアル (`tutorial/auth-jwt.md`、`tutorial/sqlmodel.md`、`tutorial/llm-agent.md`) を翻訳し、最初の翻訳以降に英語原典とずれていたページも元どおりに揃えました。`docs/ja/changelog.md` は英語の `CHANGELOG.md` を意図的に再利用しています。 | +| 🇨🇳 Chinese (`zh`) | ✅ 完了 | 29 / 29 | Phase 1〜3 は完了しています。2026-10-11 に v1.4.1 の英語原典と再同期しました: v1.4.0 で追加された 3 つのチュートリアル (`tutorial/auth-jwt.md`、`tutorial/sqlmodel.md`、`tutorial/llm-agent.md`) を翻訳し、最初の翻訳以降に英語原典とずれていたページも元どおりに揃えました。`docs/zh/changelog.md` は英語の `CHANGELOG.md` を意図的に再利用しています。 | +| 🇪🇸 Spanish (`es`) | ✅ 完了 | 29 / 29 | Phase 1〜3 は完了しています。2026-10-11 に v1.4.1 の英語原典と再同期しました: v1.4.0 で追加された 3 つのチュートリアル (`tutorial/auth-jwt.md`、`tutorial/sqlmodel.md`、`tutorial/llm-agent.md`) を翻訳し、最初の翻訳以降に英語原典とずれていたページも元どおりに揃えました。`docs/es/changelog.md` は英語の `CHANGELOG.md` を意図的に再利用しています。 | +| 🇫🇷 French (`fr`) | ✅ 完了 | 29 / 29 | Phase 1〜3 は完了しています。2026-10-11 に v1.4.1 の英語原典と再同期しました: v1.4.0 で追加された 3 つのチュートリアル (`tutorial/auth-jwt.md`、`tutorial/sqlmodel.md`、`tutorial/llm-agent.md`) を翻訳し、最初の翻訳以降に英語原典とずれていたページも元どおりに揃えました。`docs/fr/changelog.md` は英語の `CHANGELOG.md` を意図的に再利用しています。 | +| 🇩🇪 German (`de`) | ✅ 完了 | 29 / 29 | Phase 1〜3 は完了しています。2026-10-11 に v1.4.1 の英語原典と再同期しました: v1.4.0 で追加された 3 つのチュートリアル (`tutorial/auth-jwt.md`、`tutorial/sqlmodel.md`、`tutorial/llm-agent.md`) を翻訳し、最初の翻訳以降に英語原典とずれていたページも元どおりに揃えました。`docs/de/changelog.md` は英語の `CHANGELOG.md` を意図的に再利用しています。 | -*スナップショット検証日: 2026-06-18。Phase 3 (contributing + reference) を反映した現在のブランチを基準に `zh` 行を再集計しました。中国語はロケール側のページが 26 件すべて存在し、✅ 完了として追跡されます。* この表は手動で管理されています。リポジトリルートで現在の状態を再カウントしたい場合は、次のコマンドを実行してください: +*スナップショット検証日: 2026-10-11 (v1.4.1 リリース後)。翻訳対象の 6 つのロケールすべてを英語原典と同じ水準に揃えました: v1.4.0 で追加された 3 つのチュートリアルを翻訳し、最初の翻訳以降にずれていたすべてのページを再同期しました。リリースの途中では翻訳が遅れることもあります — 翻訳ページが英語と食い違う場合は英語を信頼してください。* この表は手動で管理されています。リポジトリルートで現在の状態を再カウントしたい場合は、次のコマンドを実行してください: ```console $ for loc in en ko ja zh es fr de; do @@ -39,6 +39,7 @@ $ for loc in en ko ja zh es fr de; do 凡例: - ✅ **原典 (Source of truth)** — ドキュメントを記述する際に基準とするロケールです。 +- ✅ **完了** — すべての英語ページに対応する翻訳ページがあります。ただし、リリースの途中では個々のページが英語の更新より遅れることがあります。 - 🟡 **部分** — 一部のページのみ翻訳済み。未翻訳のページは英語にフォールバックします。 - 🔴 **スケルトン** — 言語切替メニューには表示されますが、翻訳済みのページがまだリポジトリに追加されていない状態です。翻訳済みのナビゲーションラベルの下に、英語コンテンツがそのまま表示されます。 diff --git a/docs/ja/tutorial/auth-jwt.md b/docs/ja/tutorial/auth-jwt.md new file mode 100644 index 0000000..44ec24c --- /dev/null +++ b/docs/ja/tutorial/auth-jwt.md @@ -0,0 +1,294 @@ +# `fastapi-auth-jwt` による JWT 認証 + +最初のコミットから本物のアカウント機能を備えた API を構築します。このチュートリアルでは +`fastapi-auth-jwt` テンプレートを最初から最後まで取り上げます。トークンペアの発行とローテーションの仕組み、 +再送されたリフレッシュトークンが拒否される理由、ロールとスコープのガードの配線、そして SQLite から +移行するときに何が変わるのかを順に確認します。 + +## 学べること + +- `fastkit startdemo fastapi-auth-jwt` でプロジェクトを生成する +- アクセストークンとリフレッシュトークンの違い、そしてローテーションが重要な理由 +- `get_current_user`、`get_current_active_superuser`、`require_scopes(...)` がルートを保護する方法 +- パスワードのハッシュ化がどこで行われ、なぜ argon2id なのか +- 付属の SQLite ファイルから Alembic を使って PostgreSQL へ移行する + +## 前提条件 + +- Python 3.12 以上 +- FastAPI-fastkit がインストール済み (`pip install fastapi-fastkit`) +- FastAPI の依存性と pydantic スキーマに慣れていること + +FastAPI アプリを一度も作ったことがない場合は、まず +[基本 API サーバーの構築](basic-api-server.md) を読んでから戻ってきてください — このテンプレートは +基本を理解していることを前提としています。 + +## ステップ 1: プロジェクトを生成 + +```console +$ fastkit startdemo fastapi-auth-jwt +Enter the project name: accounts-api +Enter the author name: Developer Kim +Enter the author email: developer@example.com +Enter the project description: Accounts and sessions service +Select package manager (pip, uv, pdm, poetry) [uv]: uv +Do you want to proceed with project creation? [y/N]: y +``` + +次に実行します: + +```console +$ cd accounts-api +$ bash scripts/run-server.sh # または: uvicorn src.app.main:app --reload +``` + +アプリは SQLite (`sqlite:///./app.db`) 上で起動し、起動時にテーブルを作成します。そのため、事前に +インストールや設定をする必要はありません。ドキュメントは で提供されます。 + +!!! tip "コミットする前にプレビュー" + `fastkit startdemo fastapi-auth-jwt --dry-run` は何も書き込まずに、ファイルツリーとパッケージ一覧を + 出力します — これから何が手に入るのかを確認するのに便利です。 + +## ステップ 2: 生成されるツリー + +``` +accounts-api/ +├── pyproject.toml # PEP 621 メタデータ + [tool.fastapi-fastkit] +├── requirements.txt +├── alembic.ini +├── Dockerfile +├── .env # SECRET_KEY、DATABASE_URL、トークンの有効期間 +├── scripts/ +│ ├── format.sh lint.sh run-server.sh test.sh +├── src/ +│ └── app/ +│ ├── main.py # FastAPI() + lifespan + CORS + api_router +│ ├── core/ +│ │ ├── config.py # pydantic-settings +│ │ └── security.py # argon2 ハッシュ化 + JWT エンコード/デコード +│ ├── db/session.py # engine + get_session 依存性 +│ ├── api/ +│ │ ├── router.py # health + 各ドメインルーターを集約 +│ │ ├── health.py # GET /health +│ │ └── deps.py # セッション、現在のユーザー、ロール/スコープガード +│ ├── domains/ +│ │ ├── auth/ # ログイン、リフレッシュのローテーション、ログアウト +│ │ └── users/ # 登録とプロフィール +│ └── alembic/ # マイグレーション環境 + versions/ +└── tests/ + ├── conftest.py test_health.py test_auth.py test_users.py +``` + +レイアウトは `domain-starter` と同じ形です。各ドメインは自身の `router.py` (トランスポート)、 +`service.py` (ビジネスロジック)、`repository.py` (データアクセス)、`models.py` (テーブル)、 +`schemas.py` (API の入出力) を所有します。認証に関する関心事は `auth/` に、アカウントに関する関心事は +`users/` に置かれ、どちらも相手の内部には踏み込みません。 + +## ステップ 3: 登録、ログイン、保護されたルートの呼び出し + +```console +# 1. 登録 +$ curl -X POST http://127.0.0.1:8000/api/v1/auth/register \ + -H 'Content-Type: application/json' \ + -d '{"email": "alice@example.com", "password": "s3cret-password"}' + +# 2. ログイン — フォームエンコードである点に注意: これは OAuth2 パスワードフローです +$ curl -X POST http://127.0.0.1:8000/api/v1/auth/login \ + -d 'username=alice@example.com&password=s3cret-password' +{"access_token": "...", "refresh_token": "...", "token_type": "bearer"} + +# 3. 保護されたルートを呼び出す +$ curl http://127.0.0.1:8000/api/v1/users/me \ + -H "Authorization: Bearer $ACCESS_TOKEN" +``` + +Swagger の「Authorize」ボタンも同じフローを実行するので、ブラウザを離れずに `/docs` から +保護されたルートを試せます。 + +エンドポイントの全体像: + +| メソッド | エンドポイント | 認証 | 説明 | +|--------|-----------------------------|-------------|-----------------------------------| +| GET | `/api/v1/health` | — | Liveness プローブ | +| POST | `/api/v1/auth/register` | — | アカウントを作成 | +| POST | `/api/v1/auth/login` | — | OAuth2 パスワードフォーム → トークンペア | +| POST | `/api/v1/auth/refresh` | リフレッシュトークン | 新しいトークンペアにローテーション | +| POST | `/api/v1/auth/logout` | リフレッシュトークン | リフレッシュトークンを 1 つ失効させる | +| POST | `/api/v1/auth/logout-all` | アクセストークン | ユーザーのすべてのセッションを失効させる | +| GET | `/api/v1/users/me` | アクセストークン | 現在のユーザーのプロフィール | +| PATCH | `/api/v1/users/me` | アクセストークン | 現在のユーザーのプロフィールを更新 | +| GET | `/api/v1/users/me/scopes` | スコープ `me` | スコープで保護されたルートの例 | +| GET | `/api/v1/users` | スーパーユーザー | ロールで保護されたユーザー一覧 | + +## ステップ 4: 2 つのトークンとその理由 + +ログインすると **ペア** が返されます: + +- **アクセストークン** は短命 (`ACCESS_TOKEN_EXPIRE_MINUTES`、デフォルトは 30) で、すべてのリクエストに + 付けて送られます。短命なので、漏えいしてもすぐに使えなくなります。 +- **リフレッシュトークン** は長命 (`REFRESH_TOKEN_EXPIRE_DAYS`、デフォルトは 14) で、用途はただ 1 つ、 + 新しいペアを取得することだけです。 + +ここまでは珍しいことは何もありません。このテンプレートが一般的な JWT スニペットと異なるのは、 +リフレッシュトークンを **追跡してローテーションする** 点です。発行されるすべてのリフレッシュトークンには +`jti` クレームが付与され、その `jti` がデータベースに書き込まれます: + +```python +def issue_token_pair(self, user: User) -> TokenPair: + scopes = user.scope_list + access_token, _, _ = create_access_token(str(user.id), scopes) + refresh_token, jti, expires_at = create_refresh_token(str(user.id), scopes) + self.refresh_tokens.add(jti, user.id or 0, expires_at) + return TokenPair(access_token=access_token, refresh_token=refresh_token) +``` + +リフレッシュ時には、新しいペアを発行する **前に** 提示されたトークンが失効されます: + +```python +def rotate(self, refresh_token: str) -> TokenPair: + payload = decode_token(refresh_token, REFRESH_TOKEN_TYPE) + record = self.refresh_tokens.get_by_jti(str(payload.get("jti"))) + if record is None or record.revoked: + raise TokenError("refresh token is no longer valid") + ... + self.refresh_tokens.revoke(record) + return self.issue_token_pair(user) +``` + +試してみましょう: 一度リフレッシュしてから、*古い* リフレッシュトークンをもう一度送ります。 + +```console +$ curl -X POST http://127.0.0.1:8000/api/v1/auth/refresh \ + -H 'Content-Type: application/json' \ + -d "{\"refresh_token\": \"$REFRESH_TOKEN\"}" +# → 新しいペア + +$ curl -X POST http://127.0.0.1:8000/api/v1/auth/refresh \ + -H 'Content-Type: application/json' \ + -d "{\"refresh_token\": \"$REFRESH_TOKEN\"}" +# → 401: トークンは使われた瞬間に失効しています +``` + +この 1 つの性質によって、盗まれたリフレッシュトークンは永続的なバックドアではなく、検出可能な +使い捨ての失敗に変わります。 + +ログアウトも同じ仕組みで動作します。これは素朴な JWT 構成では通常実現できないもう 1 つの点です: +`POST /auth/logout` はトークンを 1 つ失効させ、`POST /auth/logout-all` はそのユーザーに属する +すべてのリフレッシュトークンを失効させます。 + +## ステップ 5: パスワード + +ハッシュ化は `src/app/core/security.py` にあり、`pwdlib` を通じて **argon2id** を使います — 旧来の +bcrypt 構成ではなく、現在推奨されているデフォルトです。router や service が平文のパスワードを目にするのは +検証する瞬間だけで、`hashed_password` がレスポンススキーマに含まれることもありません。 + +ここで設定する必要があるものはありません。唯一 *必ず* 設定しなければならないのは署名鍵です。 + +## ステップ 6: ロールとスコープ + +3 つのガードが FastAPI の依存性として付属しています: + +```python +CurrentUser = Annotated[User, Depends(get_current_user)] +``` + +- **`get_current_user`** は bearer アクセストークンをデコードしてユーザーを読み込み、無効化された + アカウントを `403` で拒否します。不正な形式のトークン、期限切れのトークン、削除済みユーザーを指す + トークンはすべて `401` になります。 +- **`get_current_active_superuser`** はその上に `is_superuser` のチェックを重ねます。 + `GET /api/v1/users` がこれを使用します。 +- **`require_scopes(...)`** は特定のスコープを要求する依存性を構築します: + +```python +from fastapi import Depends +from src.app.api.deps import require_scopes + +@router.get("/reports", dependencies=[Depends(require_scopes("reports"))]) +def read_reports() -> list[str]: + ... +``` + +スコープはユーザー行にカンマ区切りの文字列 (`scopes: str`、`scope_list` プロパティで読み出し) として +保存され、発行されるすべてのトークンにコピーされます。これはスターターとして機能する最もシンプルな形を +意図的に選んだものです。権限モデルが文字列に収まらなくなったら、このカラムをロールテーブルに置き換え、 +依存性のシグネチャはそのまま維持してください — router 側は何も変更する必要がありません。 + +## ステップ 7: 保護されたドメインを追加する + +たとえば、ログイン済みのユーザーだけがアクセスできる `orders` ドメインを追加するには: + +1. `users/` をミラーして `src/app/domains/orders/` を作成します + (`models.py`、`schemas.py`、`repository.py`、`service.py`、`router.py`)。 +2. アカウントが必要なルートでは `CurrentUser` を引数として受け取るか、router に + `Depends(require_scopes("orders"))` を付けます。 +3. `src/app/api/router.py` の `# fastkit:routes` アンカーの上に router を登録します。 + `fastkit addroute` も同じアンカーに書き込むので、手書きのルートと生成されたルートが 1 か所にまとまります。 +4. マイグレーションを生成します: `alembic revision --autogenerate -m "add orders"`。 +5. 新しいエンドポイント用に `tests/test_users.py` をミラーします。 + +## ステップ 8: 今は SQLite、後で PostgreSQL + +デフォルトの `DATABASE_URL` はローカルの SQLite ファイルで、アプリは起動時に `create_db_and_tables()` を +呼び出します。そのため `pytest` も `uvicorn` も準備なしで動作します。ローカル開発以外の用途では、 +代わりに Alembic にスキーマを管理させましょう: + +```console +$ export DATABASE_URL='postgresql+psycopg://postgres:postgres@localhost:5432/app_db' +$ alembic upgrade head +``` + +モデルを変更したら: + +```console +$ alembic revision --autogenerate -m "add profile fields" +``` + +## ステップ 9: デプロイの前に + +`.env` の 2 つの設定が、これがデモなのか本物のサービスなのかを決めます: + +- **`SECRET_KEY`** はすべてのトークンに署名します。未設定のままにすると、プロセスごとにランダムな鍵が + 生成されます — その結果、再起動のたびに全員がログアウトされ、複数のワーカー間でどのトークンが有効かの + 判断が食い違います。鍵を生成して固定してください: + + ```console + $ python -c "import secrets; print(secrets.token_urlsafe(32))" + ``` + +- **`DATABASE_URL`** はデータベースを選択します。SQLite のデフォルトは本番用のストアではありません。 + +必要に応じて `FIRST_SUPERUSER_EMAIL` と `FIRST_SUPERUSER_PASSWORD` を設定すると起動時にスーパーユーザーが +作成され、`BACKEND_CORS_ORIGINS` にフロントエンドのオリジンをカンマ区切りで設定することもできます。 + +## ステップ 10: テスト + +```console +$ bash scripts/test.sh # または: pytest +``` + +テストスイートはインメモリの SQLite データベースに対して `httpx.AsyncClient` を実行し、 +登録 → ログイン → me → リフレッシュ → ログアウトという正常系全体をカバーします。さらに、実際に重要な +拒否パス — 誤ったパスワード、不正な形式のトークン、期限切れのトークン、そしてステップ 4 の再送された +リフレッシュトークン — も検証します。認証ルールを拡張するときは、まずこれらの拒否テストを拡張してください。 +壊れたガードを捕まえてくれるのはこれらのテストです。 + +## まとめ + +- **生成**: `fastkit startdemo fastapi-auth-jwt` → + `bash scripts/run-server.sh` → ドキュメントは `/docs`。 +- **トークン**: リクエストには短命のアクセストークン、リフレッシュトークンは `jti` で追跡してローテーション + するため、再送は失敗し、ログアウトが実際に機能します。 +- **ガード**: `get_current_user`、`get_current_active_superuser`、`require_scopes(...)` を + 通常の依存性として使用します。 +- **ストレージ**: SQLite はそのまま使え、PostgreSQL は `DATABASE_URL` を設定して + `alembic upgrade head` を実行するだけです。 +- **デプロイ前に**: `SECRET_KEY` を固定し、`DATABASE_URL` を本物のデータベースに向けます。 + +## 次に学ぶこと + +- [SQLModel による永続化](sqlmodel.md) — 同じドメインレイアウトで、非同期データアクセス、 + マイグレーション、汎用 CRUD に焦点を当てたチュートリアルです。 +- [ドメイン指向プロジェクト](domain-starter.md) — このテンプレートの土台となるレイアウトです + (認証の仕組みは含みません)。 +- [どのスターターを選ぶべき?](../user-guide/choosing-a-starter.md) — このテンプレートを + 他のテンプレートと比較します。 diff --git a/docs/ja/tutorial/llm-agent.md b/docs/ja/tutorial/llm-agent.md new file mode 100644 index 0000000..ef474d3 --- /dev/null +++ b/docs/ja/tutorial/llm-agent.md @@ -0,0 +1,239 @@ +# `fastapi-llm-agent` によるストリーミング LLM エージェント + +生成されたトークンをそのままストリーミングし、実際に *行動* できる — ツールを呼び出し、その結果を読み、 +処理を続けられる — チャットバックエンドを構築します。このチュートリアルでは `fastapi-llm-agent` +テンプレートを取り上げます。SSE イベントプロトコル、ツール呼び出しループとその反復上限、インターフェースの +背後にある会話メモリ、そしてネットワークに一切触れないテストスイートを順に確認します。 + +## 学べること + +- `fastkit startdemo fastapi-llm-agent` でプロジェクトを生成する +- クライアントが処理すべき SSE イベントの種類と、それぞれの意味 +- ツール呼び出しループの 1 ターンがどのように動くか (ステップごとに) +- 独自のツールを追加する +- プロセス内の会話ストアを本物のストアに差し替える +- テストが決定的で、無料で実行できる理由 + +## 前提条件 + +- Python 3.12 以上 +- FastAPI-fastkit がインストール済み (`pip install fastapi-fastkit`) +- Anthropic API キー — 本物のモデルと対話する場合にのみ必要です。生成されるテストスイートはキーなしで実行できます。 +- `async`/`await` と非同期ジェネレーターに慣れていること + +## ステップ 1: 生成と設定 + +```console +$ fastkit startdemo fastapi-llm-agent +Enter the project name: support-agent +Enter the author name: Developer Kim +Enter the author email: developer@example.com +Enter the project description: Streaming support assistant +Select package manager (pip, uv, pdm, poetry) [uv]: uv +Do you want to proceed with project creation? [y/N]: y +``` + +最初に実行する前に、キーを `.env` に設定します: + +```bash +ANTHROPIC_API_KEY=sk-ant-... +LLM_MODEL=claude-sonnet-5 +LLM_MAX_TOKENS=4096 +MAX_TOOL_ITERATIONS=5 +``` + +キーは pydantic-settings によって読み込まれ、構築時にクライアントへ渡されます。ソースコードに書き込まれる +ことも、Docker イメージに焼き込まれることもありません。 + +```console +$ cd support-agent +$ bash scripts/run-server.sh +$ bash scripts/chat.sh "What is 12 * 34?" +``` + +!!! tip "Anthropic API の代わりにゲートウェイを使う" + `LLM_BASE_URL` (ゲートウェイが bearer 認証を使う場合は `LLM_AUTH_TOKEN` も) を設定すると、LiteLLM、 + 社内プロキシ、ローカルサーバーを向けられます。通信形式は Anthropic Messages API のままです。 + 他のプロバイダー向けの変換はアプリではなくゲートウェイの役割です。 + +## ステップ 2: 生成されるツリー + +``` +support-agent/ +├── pyproject.toml # PEP 621 メタデータ + [tool.fastapi-fastkit] +├── requirements.txt +├── Dockerfile +├── .env +├── scripts/ +│ ├── chat.sh # 起動中のサーバーにメッセージを 1 件送信 +│ ├── format.sh lint.sh run-server.sh test.sh +├── src/ +│ └── app/ +│ ├── main.py # FastAPI アプリ + lifespan +│ ├── core/config.py # pydantic-settings (API キーはここ) +│ ├── llm/ +│ │ ├── client.py # AsyncAnthropic ファクトリ +│ │ ├── tools.py # ツールスキーマ + ハンドラ +│ │ └── agent.py # ストリーミングエージェント + ツールループ +│ ├── memory/ +│ │ ├── base.py # ConversationStore インターフェース +│ │ └── in_memory.py # プロセスローカルの実装 +│ ├── schemas/chat.py # リクエストボディ + SSE イベントエンベロープ +│ └── api/ +│ ├── router.py # health + chat を集約 +│ ├── deps.py # store / agent の依存性 +│ ├── health.py # GET /health +│ └── chat.py # POST /chat (SSE)、会話履歴 +└── tests/ + ├── conftest.py # スクリプト化された偽の Anthropic クライアント + ├── test_health.py test_chat.py test_tools.py test_memory.py +``` + +最初に注目しておきたい境界が 3 つあります: **クライアント** はファクトリで作成される (そのためテストで +差し替えられる)、**メモリ** は単純な実装を持つインターフェースである (そのため本番環境で差し替えられる)、 +そして **ツール** はスキーマのリストとハンドラの dict で構成される (そのため 1 つ追加しても触るファイルは +1 つだけ) という点です。 + +## ステップ 3: エンドポイント + +| メソッド | エンドポイント | 説明 | +|--------|------------------------------|-------------------------| +| GET | `/api/v1/health` | Liveness プローブ | +| POST | `/api/v1/chat` | 返答を SSE でストリーミング | +| GET | `/api/v1/conversations/{id}` | 保存された履歴を読む | +| DELETE | `/api/v1/conversations/{id}` | 会話を破棄する | + +`POST /api/v1/chat` は `{"message": "...", "conversation_id": "..."}` を受け取ります。 +新しい会話を始めるときは `conversation_id` を省略し、割り当てられた id を最後の `done` イベントから +読み取ります。 + +## ステップ 4: ストリームプロトコル + +各 SSE メッセージは、`event:` フィールドに種類を、`data:` に JSON エンベロープを持ちます。 +クライアントは 5 種類のイベントとエラーを処理する必要があります: + +| イベント | フィールド | 意味 | +|---------------|---------------------------------|--------------------------------| +| `token` | `text` | アシスタントの返答の一部 | +| `tool_use` | `tool_name`, `tool_input` | Claude がツールを要求した | +| `tool_result` | `tool_name`, `tool_output` | ツールが実行された | +| `usage` | `input_tokens`, `output_tokens` | ターンごとのトークン消費量 | +| `done` | `conversation_id` | ターンが完了した | +| `error` | `detail` | ストリームの途中で何かが失敗した | + +`tool_use` と `tool_result` は飾りではありません — ツールの実行中に、説明のない待ち時間ではなく +「検索中…」と UI に表示できるのはこれらのおかげです。 + +トークン使用量はターンごとに `app.agent` ロガーにも書き込まれるため、クライアントに計測を組み込まなくても +サーバーログで消費量を確認できます。 + +## ステップ 5: ループの 1 ターン + +`src/app/llm/agent.py` の `ChatAgent.stream` がエンジンのすべてです。1 ターンは次のように進みます: + +1. ストアから会話履歴を読み込みます。 +2. 履歴、新しいユーザーメッセージ、ツールスキーマを使ってストリーミングリクエストを開始します。 +3. テキストのチャンクが届くたびに、`token` イベントとして yield します。 +4. ストリームが終わったら最終メッセージを読み、`usage` イベントを送出してトークン数をログに記録します。 +5. **`stop_reason != "tool_use"` の場合、** アシスタントの処理は完了です: ターンを永続化し、`done` を + 送出して return します。 +6. **そうでない場合、** 各 `tool_use` ブロックについて: `tool_use` を送出し、ハンドラを実行し、 + `tool_result` を送出して、出力を集めます。 +7. すべてのツール結果を 1 つの `user` メッセージとして送り返し — これは Messages API が要求する形です — + ステップ 2 からループします。 + +ループは `MAX_TOOL_ITERATIONS` (デフォルトは 5) で上限が設けられています。上限を超えても Claude が +ツールを要求し続ける場合、サーバーが空回りし続けるのではなく、ターンが永続化され、警告がログに記録され、 +`error` イベントが送出されます: + +```python +yield AgentEvent( + type="error", + detail=( + "The assistant exceeded the tool-call limit of " + f"{self._max_tool_iterations} iterations." + ), +) +``` + +エージェントループにおいて、このような上限は任意ではありません。上限がなければ、ツールの結果を誤解した +モデルが際限なく課金し続ける可能性があります。 + +## ステップ 6: 付属のツール + +| ツール | 目的 | +|----------------|----------------------------------------------------------| +| `calculator` | 算術計算。AST に解析してホワイトリストで制限 — `eval` は使わない | +| `current_time` | IANA タイムゾーンでの現在時刻 | + +`calculator` はもう一度よく見ておく価値があります。式を `ast` で解析し、ホワイトリストに含まれるノード型 +だけを辿るため、モデルから渡されたツール引数が任意の Python を実行することはできません。モデルが生成した +入力を受け付けるツールには、すべて同じ規律が必要です: **モデルの出力は信頼できない入力です。** + +### ツールを追加する + +すべては `src/app/llm/tools.py` にあります: + +1. `TOOL_SCHEMAS` にスキーマを追加します — `name`、`description`、JSON Schema の `input_schema`。 +2. 同じ名前で `TOOL_HANDLERS` にハンドラを登録します。 + +正しく押さえておくべき点が 2 つあります: + +- **説明文は、そのツールをいつ使うべきかについて Claude が受け取る指示のすべてです。** これはドキュメント + ではなくプロンプトエンジニアリングです — 曖昧な説明文からは、一度も呼ばれないツールか、絶えず呼ばれる + ツールが生まれます。 +- **ハンドラは文字列を返し、自身の失敗は例外を送出するのではなく読みやすいメッセージに変換するべきです。** + ループはそのテキストをそのままモデルに返すため、「X という名前の都市は見つかりませんでした」であれば + モデルは立て直せますが、例外はターンを終わらせてしまいます。 + +## ステップ 7: 会話メモリ + +`ConversationStore` (`src/app/memory/base.py`) がインターフェースで、付属の `in_memory.py` は dict で +それを実装しています。単一プロセスならこれで十分ですが、それ以外の用途には向きません — 再起動するとすべて +忘れてしまい、2 つのワーカー間で履歴が食い違います。 + +本格的にするには: 同じインターフェースを Redis やデータベースに対して実装し、`src/app/api/deps.py` の +`get_store()` からそれを返します。エージェントも router も具象クラスの名前を一切参照していないため、 +どちらも変更する必要はありません。 + +## ステップ 8: テスト + +```console +$ bash scripts/test.sh # または: pytest +``` + +`tests/conftest.py` は、`AsyncAnthropic.messages.stream` を模倣する偽物を通じてスクリプト化された +ターンを再生します。そのためテストスイートは決定的で、オフラインで実行でき、費用もかかりません。それでいて、 +重要なパス — ストリーミングのパスと、ツールが呼び出されてからモデルが答えるターンを含むツールループ全体 — +をカバーします。 + +エージェントを拡張するときも、このパターンを守りましょう。LLM 連携を本物のモデルに対してテストすると、 +遅く、不安定で、高価なうえに、モデルが何を言うかをアサートすることもできないテストスイートになります。 +スクリプト化されたクライアントに対して *ループ* をテストすれば、自分が書いたものをそのままアサートできます。 + +## ステップ 9: 拡張ポイント + +- **永続的な履歴** — `ConversationStore` を Redis やデータベースに対して実装します (ステップ 7)。 +- **RAG** — 検索 (retrieval) はテンプレートの対象外として意図的に外しています。自然な接合点は + `ChatAgent.stream` です: 最初のリクエストの前にコンテキストを取得してシステムプロンプトの先頭に付け加えるか、 + インデックスをもう 1 つのツールとして公開し、いつ検索するかをモデルに判断させます。 +- **認証とレート制限** — 出荷時の `POST /chat` は認証なしです。公開する前に router に依存性を + 付けてください。再利用できるガードは [JWT 認証チュートリアル](auth-jwt.md) にあります。 + +## まとめ + +- **生成**: `fastkit startdemo fastapi-llm-agent`、`ANTHROPIC_API_KEY` を設定、 + `bash scripts/run-server.sh`。 +- **ストリーミング**: `token` / `tool_use` / `tool_result` / `usage` / `done` / `error` イベントを + 使う SSE。 +- **ループ**: ストリーム → ツール呼び出し → 結果を 1 つの user メッセージとして返す → 繰り返し。 + `MAX_TOOL_ITERATIONS` で上限を設定。 +- **ツール**: `TOOL_SCHEMAS` のスキーマと `TOOL_HANDLERS` のハンドラ。ツールの入力は信頼できないものとして扱う。 +- **メモリ**: `ConversationStore` インターフェースと dict による実装。`get_store()` で差し替え。 +- **テスト**: スクリプト化された偽のクライアントにより、テストスイートはオフラインかつ決定的。 + +## 次に学ぶこと + +- [MCP との統合](mcp-integration.md) — チャットエージェントではなく MCP サーバーが必要な場合。 +- [JWT 認証](auth-jwt.md) — チャットエンドポイントの前にアカウントを置く方法。 +- [どのスターターを選ぶべき?](../user-guide/choosing-a-starter.md) diff --git a/docs/ja/tutorial/mcp-integration.md b/docs/ja/tutorial/mcp-integration.md index 835b390..47ab222 100644 --- a/docs/ja/tutorial/mcp-integration.md +++ b/docs/ja/tutorial/mcp-integration.md @@ -68,8 +68,8 @@ Deploying FastAPI project using 'fastapi-mcp' template │ Dependency 1 │ fastapi │ │ Dependency 2 │ uvicorn │ │ Dependency 3 │ pydantic │ -│ Dependency 4 │ python-jose │ -│ Dependency 5 │ passlib │ +│ Dependency 4 │ PyJWT │ +│ Dependency 5 │ pwdlib[argon2] │ │ Dependency 6 │ python-multipart│ │ Dependency 7 │ mcp │ └──────────────┴────────────────┘ @@ -131,21 +131,21 @@ ai-integrated-api/ ```python from datetime import datetime, timedelta from typing import Optional, Dict, Any -from jose import JWTError, jwt -from passlib.context import CryptContext +import jwt +from pwdlib import PasswordHash from src.core.config import settings -# パスワードハッシュ化 -pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") +# パスワードハッシュ化 — 新規アプリケーションに推奨されるデフォルトである argon2id。 +password_hash = PasswordHash.recommended() def verify_password(plain_password: str, hashed_password: str) -> bool: """パスワード検証""" - return pwd_context.verify(plain_password, hashed_password) + return password_hash.verify(plain_password, hashed_password) def get_password_hash(password: str) -> str: """パスワードハッシュ化""" - return pwd_context.hash(password) + return password_hash.hash(password) def create_access_token(data: Dict[str, Any], expires_delta: Optional[timedelta] = None) -> str: """アクセストークン生成""" @@ -189,7 +189,7 @@ def decode_token(token: str) -> Optional[Dict[str, Any]]: algorithms=[settings.ALGORITHM] ) return payload - except JWTError: + except jwt.PyJWTError: return None def verify_token(token: str, token_type: str = "access") -> Optional[str]: @@ -452,8 +452,7 @@ user_db = UserDatabase() from typing import Optional, List from fastapi import Depends, HTTPException, status, Security from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials, APIKeyHeader -from jose import JWTError - +import jwt from src.auth.jwt_handler import decode_token, token_manager from src.auth.models import User, UserInDB, Permission, user_db @@ -486,7 +485,7 @@ async def get_current_user( if user_id is None: raise credentials_exception - except JWTError: + except jwt.PyJWTError: raise credentials_exception user = user_db.get_user_by_id(user_id) diff --git a/docs/ja/tutorial/sqlmodel.md b/docs/ja/tutorial/sqlmodel.md new file mode 100644 index 0000000..fee2728 --- /dev/null +++ b/docs/ja/tutorial/sqlmodel.md @@ -0,0 +1,293 @@ +# `fastapi-sqlmodel` による非同期永続化 + +各概念を **一度だけ** 定義するデータベース駆動の API を構築します。SQLModel のクラスは +SQLAlchemy のテーブルであると同時に pydantic のスキーマでもあるため、カラムを 1 つ追加するために +4 つのファイルを編集する必要はありません。このチュートリアルでは `fastapi-sqlmodel` テンプレートを +取り上げます。モデル階層、汎用 CRUD ベース、ページネーションされたレスポンス、非同期 Alembic +マイグレーション、そして独自のドメインを追加する方法を順に確認します。 + +## 学べること + +- `fastkit startdemo fastapi-sqlmodel` でプロジェクトを生成する +- `ItemBase` → `Item` / `ItemCreate` / `ItemUpdate` / `ItemPublic` がスキーマの重複をなくす仕組み +- `CRUDBase` が標準で提供するものと、拡張すべき場所 +- `Page[T]` のリスト用エンベロープと、その上限がどこで適用されるか +- 非同期 Alembic マイグレーションの実行と、SQLite から PostgreSQL への切り替え + +## 前提条件 + +- Python 3.12 以上 +- FastAPI-fastkit がインストール済み (`pip install fastapi-fastkit`) +- `async`/`await` と基本的な SQL の概念に慣れていること + +同期型で Compose ベースの PostgreSQL スタックが必要な場合は、`fastapi-psql-orm` を使う +[データベース統合](database-integration.md) を参照してください。 + +## ステップ 1: 生成と実行 + +```console +$ fastkit startdemo fastapi-sqlmodel +Enter the project name: catalog-api +Enter the author name: Developer Kim +Enter the author email: developer@example.com +Enter the project description: Product catalog service +Select package manager (pip, uv, pdm, poetry) [uv]: uv +Do you want to proceed with project creation? [y/N]: y +``` + +```console +$ cd catalog-api +$ bash scripts/migrate.sh # alembic upgrade head +$ bash scripts/run-server.sh # または: uvicorn src.app.main:app --reload +``` + +デフォルトの `DATABASE_URL` は `sqlite+aiosqlite:///./app.db` なので、データベースサーバーを +インストールする必要はありません。ドキュメントは で提供されます。 + +## ステップ 2: 生成されるツリー + +``` +catalog-api/ +├── pyproject.toml # PEP 621 メタデータ + [tool.fastapi-fastkit] +├── requirements.txt +├── alembic.ini +├── Dockerfile +├── .env +├── scripts/ +│ ├── format.sh lint.sh migrate.sh run-server.sh test.sh +├── src/ +│ └── app/ +│ ├── main.py # FastAPI アプリ + lifespan +│ ├── core/config.py # pydantic-settings +│ ├── db/ +│ │ ├── session.py # 非同期エンジン、get_session 依存性 +│ │ └── base.py # Alembic 用のメタデータ集約 +│ ├── crud/ +│ │ ├── base.py # 汎用 CRUDBase +│ │ └── pagination.py # Page[T] エンベロープ +│ ├── alembic/ +│ │ ├── env.py # 非同期マイグレーション環境 +│ │ └── versions/0001_create_items_table.py +│ ├── api/ +│ │ ├── router.py # health + ドメインルーターを集約 +│ │ └── health.py # GET /health +│ └── domains/items/ # 例題のドメイン +│ ├── models.py # Item テーブル + Create/Update/Public スキーマ +│ ├── crud.py # ItemCRUD(CRUDBase) +│ └── router.py # FastAPI ルーター +└── tests/ + ├── conftest.py test_health.py test_items.py test_crud_base.py +``` + +ドメインフォルダに *含まれていない* ものに注目してください: `models.py` とは別の `schemas.py` はなく、 +ドメインごとの repository のボイラープレートもありません。前者は SQLModel に、後者は `CRUDBase` に +吸収されています。 + +## ステップ 3: 概念ごとに 1 つのモデル定義 + +`src/app/domains/items/models.py` を開きます。すべてが 1 つのベースから派生しています: + +```python +class ItemBase(SQLModel): + """テーブルとすべてのリクエスト/レスポンススキーマで共有されるフィールド。""" + name: str = Field(min_length=1, max_length=128, index=True) + description: str | None = Field(default=None, max_length=512) + price: float = Field(ge=0) + in_stock: bool = True + + +class Item(ItemBase, table=True): # 実際のテーブル + __tablename__ = "items" + id: int | None = Field(default=None, primary_key=True) + created_at: datetime = Field(default_factory=_utcnow) + + +class ItemCreate(ItemBase): ... # POST ボディ — サーバーが割り当てるフィールドなし + + +class ItemUpdate(SQLModel): # PATCH ボディ — すべてのフィールドが任意 + name: str | None = Field(default=None, min_length=1, max_length=128) + ... + + +class ItemPublic(ItemBase): # レスポンス — ベース + サーバーが割り当てるフィールド + id: int + created_at: datetime +``` + +4 つのクラスがそれぞれ別の役割を持ち、フィールドが定義される場所は 1 か所だけです: + +| クラス | 役割 | 分離されている理由 | +|--------------|-------------------------------|-------------------| +| `ItemBase` | 共有フィールドの定義 | 唯一の情報源 | +| `Item` | データベーステーブル | `id`、`created_at` を追加 | +| `ItemCreate` | `POST` のリクエストボディ | `id` を受け付けてはならない | +| `ItemUpdate` | `PATCH` のリクエストボディ | すべてのフィールドが任意 | +| `ItemPublic` | レスポンスボディ | サーバーが割り当てたフィールドを公開 | + +**得られるもの:** `sku` カラムを追加するには、`ItemBase` に 1 行追加するだけです。テーブルにカラムが +追加され、作成用ボディがそれを受け付け、レスポンスがそれを返します — ほかの編集は不要です。 +`ItemUpdate` は意図的に明示的なままにしています: 部分更新のセマンティクスは派生させるものではなく、 +決めるべきものだからです。 + +## ステップ 4: 書き直さなくてよい CRUD + +`src/app/crud/base.py` は、テーブルモデルとその 2 つの入力スキーマでパラメータ化された汎用クラスです: + +```python +class CRUDBase(Generic[ModelType, CreateSchemaType, UpdateSchemaType]): + async def get(self, session, obj_id) -> ModelType | None: ... + async def list(self, session, *, offset=0, limit=20) -> Sequence[ModelType]: ... + async def count(self, session) -> int: ... + async def create(self, session, obj_in) -> ModelType: ... + async def update(self, session, db_obj, obj_in) -> ModelType: ... + async def delete(self, session, db_obj) -> None: ... +``` + +しっかり理解しておきたい細部が 1 つあります — `update` は `exclude_unset=True` を使用します: + +```python +changes: dict[str, Any] = obj_in.model_dump(exclude_unset=True) +``` + +これが `PATCH` を本当の部分更新にしています。クライアントが送らなかったフィールドは dict に含まれない +ため、書き込まれることはありません。`exclude_unset` がなければ、`PATCH` は触れていないカラムを +スキーマのデフォルト値で黙って上書きしてしまいます。 + +ドメインはベースをサブクラス化し、そのドメイン固有のものだけを追加します: + +```python +class ItemCRUD(CRUDBase[Item, ItemCreate, ItemUpdate]): + """名前検索を追加した Item 用の CRUD。""" + + async def search_by_name(self, session, term, *, limit=20): + statement = select(Item).where(Item.name.like(f"%{term}%")).limit(limit) + result = await session.exec(statement) + return result.all() + + +item_crud = ItemCRUD(Item) +``` + +## ステップ 5: ページネーションされたリスト + +リスト系エンドポイントは、素の配列ではなく `Page[T]` エンベロープを返します: + +```json +{"items": [...], "total": 42, "offset": 0, "limit": 20} +``` + +上限の切り詰めはクライアントではなく router で行われます: + +```python +limit: int = Query(default=settings.DEFAULT_PAGE_LIMIT, ge=1) +... +limit = min(limit, settings.MAX_PAGE_LIMIT) +``` + +そのため、`?limit=100000` で 1 回のリクエストをテーブル全体のスキャンに変えることはできません。 +`Page` は `offset`、`len(items)`、`total` から導出される `has_next` プロパティも公開しており、 +これはたいていフロントエンドが実際に必要とするものです。 + +items ドメインが提供するエンドポイント: + +| メソッド | エンドポイント | 説明 | +|--------|---------------------------|--------------------------------| +| GET | `/api/v1/health` | Liveness プローブ | +| GET | `/api/v1/items` | items の一覧 (`offset`、`limit`) | +| GET | `/api/v1/items/{item_id}` | item を 1 件取得 | +| POST | `/api/v1/items` | item を作成 (`201`) | +| PATCH | `/api/v1/items/{item_id}` | item を部分更新 | +| DELETE | `/api/v1/items/{item_id}` | item を削除 (`204`) | + +試してみましょう: + +```console +$ curl -X POST http://127.0.0.1:8000/api/v1/items \ + -H 'Content-Type: application/json' \ + -d '{"name": "Keyboard", "price": 89.0}' + +$ curl 'http://127.0.0.1:8000/api/v1/items?offset=0&limit=5' +``` + +## ステップ 6: 端から端まで非同期 + +`src/app/db/session.py` はプロセスごとに 1 つの `AsyncEngine` を構築し、リクエストスコープの +セッションを払い出します: + +```python +async def get_session() -> AsyncGenerator[AsyncSession, None]: + async with async_session_factory() as session: + yield session +``` + +router はこれに依存し (`session: AsyncSession = Depends(get_session)`)、`CRUDBase` はこれを第 1 引数と +して受け取り、すべてのデータベース呼び出しは await されます。リクエストパス上で I/O のためにイベント +ループをブロックするものはありません。 + +`create_db_and_tables()` もここにあり、アプリの `lifespan` から呼び出されます。これは SQLite の +デフォルトとテストのための便宜的な機能です — Alembic がスキーマを管理するようになったら、 +`CREATE_TABLES_ON_STARTUP=false` を設定してください。 + +## ステップ 7: ドメインを追加する + +1. `items` をミラーして `src/app/domains//` を作成します + (`models.py`、`crud.py`、`router.py`)。 +2. **`src/app/db/base.py` でテーブルモデルを import します。** これは忘れやすく、デバッグで混乱しやすい + ステップです: Alembic の autogenerate と `create_db_and_tables` は import 済みのモデルしか認識しない + ため、import を忘れるとエラーにはならず、空のマイグレーションとして現れます。 +3. `src/app/api/router.py` に router を登録します。`# fastkit:imports` と `# fastkit:routes` の + アンカーは `fastkit addroute` がコードを挿入する場所を示しています — 自分のコードも同じ場所に置きましょう。 +4. マイグレーションを生成します: + `alembic revision --autogenerate -m "add "`。 +5. `tests/test_items.py` をミラーして `tests/test_.py` を追加します。 + +## ステップ 8: マイグレーションと PostgreSQL + +`src/app/alembic/env.py` 配下の Alembic 環境は非同期なので、アプリと同じドライバスタックで動作します。 +日常的な操作: + +```console +$ bash scripts/migrate.sh # upgrade head +$ alembic revision --autogenerate -m "add sku to items" # モデル変更後 +``` + +データベースの切り替えは、URL の変更とドライバの追加だけです: + +```console +$ uv pip install "asyncpg>=0.31.0" +$ export DATABASE_URL="postgresql+asyncpg://user:password@localhost:5432/app" +$ bash scripts/migrate.sh +``` + +モデル、CRUD、router は何も変わりません — ドライバを `create_engine` の背後に置いておく目的はそこにあります。 + +## ステップ 9: テスト + +```console +$ bash scripts/test.sh # または: pytest +``` + +各テストは新しいインメモリの SQLite データベースに対して実行されます: マイグレーションも、フィクスチャ +ファイルも、テスト間のクリーンアップも不要です。`test_crud_base.py` は汎用ベースを直接検証します — +拡張する前に読んでおく価値があります。自分のドメインが継承する `exclude_unset` の振る舞いと +count/offset の契約がそこに記述されているからです。 + +## まとめ + +- **生成**: `fastkit startdemo fastapi-sqlmodel` → + `bash scripts/migrate.sh` → `bash scripts/run-server.sh`。 +- **モデル**: `ItemBase` がフィールドを保持し、テーブルと入出力スキーマはそこから派生するため、 + 新しいカラムは一度だけ宣言すれば済みます。 +- **CRUD**: `CRUDBase` が get / list / count / create / update / delete を提供し、ドメインは独自の + クエリのためにそれをサブクラス化します。 +- **リスト**: サーバー側で上限を切り詰める `Page[T]` エンベロープ。 +- **スキーマ**: 非同期 Alembic。新しいモデルは `src/app/db/base.py` で import するのを忘れずに。 +- **データベース**: デフォルトは SQLite、`DATABASE_URL` を変更して `asyncpg` をインストールすれば PostgreSQL。 + +## 次に学ぶこと + +- [JWT 認証](auth-jwt.md) — このデータ層の前にアカウントと保護されたルートを置きます。 +- [ドメイン指向プロジェクト](domain-starter.md) — 両テンプレートが共有するレイアウトです + (データベースは含みません)。 +- [どのスターターを選ぶべき?](../user-guide/choosing-a-starter.md) diff --git a/docs/ja/user-guide/choosing-a-starter.md b/docs/ja/user-guide/choosing-a-starter.md index aeb6475..bfbccbf 100644 --- a/docs/ja/user-guide/choosing-a-starter.md +++ b/docs/ja/user-guide/choosing-a-starter.md @@ -17,6 +17,9 @@ FastAPI-fastkit には、プロジェクトを始めるための方法がいく | 可能な限り小さいスキャフォールドが欲しい | `fastkit init --interactive` (preset: **`minimal`**) | | 簡単なプロトタイプ / 単一ファイルのスクリプトを書く | `fastkit init --interactive` (preset: **`single-module`**) | | 実際のデータベースが必要 (PostgreSQL + SQLAlchemy + Alembic) | `fastkit startdemo fastapi-psql-orm` | +| マイグレーションと汎用 CRUD を備えた非同期の永続化が欲しい | `fastkit startdemo fastapi-sqlmodel` | +| 初日からユーザーアカウント、ログイン、保護されたルートが必要 | `fastkit startdemo fastapi-auth-jwt` | +| ストリーミングとツールを備えた LLM チャットバックエンドを作る | `fastkit startdemo fastapi-llm-agent` | | 中規模 API のための本番志向のドメインレイアウトが欲しい | `fastkit init --interactive` (preset: **`domain-starter`**) | ## `startdemo` と `init --interactive` の違いは? @@ -112,11 +115,53 @@ $ fastkit init --interactive | `fastapi-empty` | `minimal` | 素のスキャフォールド。`minimal` と同じ形になります。 | | `fastapi-single-module` | `single-module` | 単一ファイルのデモ。 | | `fastapi-domain-starter` | `domain-starter` | 推奨される現代的デフォルト。items ドメインの例を同梱。 | -| `fastapi-async-crud` | `classic-layered` | `fastapi-default` の非同期版。 | +| `fastapi-auth-jwt` | `domain-starter` | API にアカウントが必要な場合。アクセス/リフレッシュトークン、argon2id ハッシュ化、ロールとスコープ、Alembic マイグレーション。 | +| `fastapi-sqlmodel` | `domain-starter` | 非同期の永続化を第一に: SQLModel + 非同期 SQLAlchemy、マイグレーション、汎用 CRUD ベース、ページネーションされたリスト。 | +| `fastapi-llm-agent` | `domain-starter` | 本物のツール呼び出しループとオフラインのテストを備えた、ストリーミング Claude チャットバックエンド。 | | `fastapi-custom-response` | `classic-layered` | カスタムレスポンスエンベロープ / フォーマットのデモ。 | -| `fastapi-dockerized` | `classic-layered` | デフォルトレイアウトに本番用の Dockerfile を追加。 | -| `fastapi-psql-orm` | (直接対応するプリセットなし) | PostgreSQL + SQLAlchemy + Alembic。実際のデータベースが必要なときに選択。 | +| `fastapi-psql-orm` | (直接対応するプリセットなし) | PostgreSQL + SQLAlchemy + Alembic、同期型。実際のデータベースを中心にした Compose スタックが欲しいときに選択。 | | `fastapi-mcp` | (直接対応するプリセットなし) | Model Context Protocol 連携。 | +| `fastapi-async-crud` | `classic-layered` | ⚠️ **非推奨。** `fastapi-default` の非同期版。`fastapi-sqlmodel` を推奨。 | +| `fastapi-dockerized` | `classic-layered` | ⚠️ **非推奨。** デフォルトレイアウトに Dockerfile を追加。現在のテンプレートにはすべて Dockerfile が含まれています。 | + +### 3 つの新しいスターター + +v1.4.0 で追加された 3 つのスターターはいずれも `domain-starter` の形 (概念ごとのフォルダを持つ `src/app/`) +に従っているため、そのプリセットですでに知っている内容とそのまま組み合わせられます。 + +**`fastapi-auth-jwt` — 本番環境に出しても通用する認証。** +アクセストークンは短命です。リフレッシュトークンは使うたびにローテーションされ、データベース上で `jti` によって +追跡されるため、再送されたトークンは黙って受け入れられるのではなく拒否されます。パスワードは `pwdlib` を介して +argon2id でハッシュ化されます。ログアウトも実際に機能し、1 つのセッション、またはユーザーのすべてのセッションを +失効させられます。ガードは依存性として付属します — `get_current_user`、`get_current_active_superuser`、そして +`require_scopes(...)` ファクトリです。外部サービスなしで SQLite 上で動作し、`DATABASE_URL` を設定すれば +PostgreSQL に移行できます。ウォークスルー: [JWT 認証](../tutorial/auth-jwt.md)。 + +**`fastapi-sqlmodel` — 概念ごとに 1 つのモデル定義。** +SQLModel のクラスは SQLAlchemy のテーブルであると同時に pydantic のスキーマでもあるため、新しいカラムは一度 +宣言するだけで、すべてのリクエスト/レスポンススキーマがそれを継承します。端から端まで非同期で、非同期 Alembic +マイグレーション、create / read / list / count / update / delete を提供して各ドメインが固有の部分だけを書けば +済むようにする `CRUDBase`、そしてページ用のエンベロープで包まれたリスト系エンドポイントを備えています。 +ウォークスルー: [SQLModel による永続化](../tutorial/sqlmodel.md)。 + +**`fastapi-llm-agent` — `POST /completion` のラッパーではなく、チャットバックエンド。** +トークンは SSE でクライアントにストリーミングされ、ツール呼び出しループはモデルの処理が終わるか反復上限に +達するまで実行されます。会話履歴は `ConversationStore` インターフェースの背後にあるため、同梱の dict から +Redis への移行は実装を 1 つ用意するだけで済みます。テストスイートは Anthropic クライアントをスクリプト化された +偽物に置き換えます — 決定的で、オフラインで動作し、API キーも不要です。 +ウォークスルー: [LLM エージェント](../tutorial/llm-agent.md)。 + +### 非推奨テンプレートについて + +`fastapi-async-crud` と `fastapi-dockerized` は引き続き同梱されており、動作するプロジェクトを生成します — +使っても何も壊れません。非推奨とされているのは、それらを選ぶ理由がなくなったからです: + +- `fastapi-dockerized` は Dockerfile を追加するために存在していました。現在のテンプレートにはすべて + Dockerfile が含まれており、対話型のデプロイ選択はどのプリセットに対しても Docker ファイルを生成します。 +- `fastapi-async-crud` はモックストアに対する非同期エンドポイントのデモでした。`fastapi-sqlmodel` は同じことを + 本物の非同期データベースに対して、マイグレーション付きで行います。 + +どちらかのテンプレートから生成された既存のプロジェクトについて、対応は必要ありません。 `fastkit list-templates` で 1 行説明付きの最新リストを確認できます。 @@ -141,5 +186,8 @@ $ fastkit init --interactive - [クイックスタート](quick-start.md) — 実際に最初のプロジェクトを作成。 - [プロジェクトの作成](creating-projects.md) — CLI フラグのより踏み込んだウォークスルー。 +- [JWT 認証チュートリアル](../tutorial/auth-jwt.md)、[SQLModel による永続化チュートリアル](../tutorial/sqlmodel.md)、 + [LLM エージェントチュートリアル](../tutorial/llm-agent.md) — v1.4.0 で追加された 3 つのスターターの + エンドツーエンドのウォークスルー。 - [ドメイン指向プロジェクトのチュートリアル](../tutorial/domain-starter.md) — `domain-starter` を選んだ場合の、生成ツリー、同梱の `items` 例、次のドメインの追加方法までを通したエンドツーエンドの解説。 - [アーキテクチャプリセットマトリクス](../reference/preset-feature-matrix.md) — プリセット / 機能ごとの完全な契約。 diff --git a/docs/ja/user-guide/cli-reference.md b/docs/ja/user-guide/cli-reference.md index 1235793..f2ea175 100644 --- a/docs/ja/user-guide/cli-reference.md +++ b/docs/ja/user-guide/cli-reference.md @@ -60,9 +60,146 @@ $ fastkit init [OPTIONS] | オプション | 説明 | デフォルト | |---|---|---| +| `--interactive` | ガイド付きセットアップ: アーキテクチャプリセットを選び、続いて機能を選択 | off | +| `--config ` | 保存済みの設定ファイルから、プロンプトなしでプロジェクトを作成 | - | +| `--save-config ` | `--interactive` と併用し、回答した設定をこのパスに書き出す | - | +| `--project-name` | プロジェクト名 (省略時はプロンプトで入力) | - | +| `--author` | 作者名 (省略時はプロンプトで入力) | - | +| `--author-email` | 作者のメールアドレス (省略時はプロンプトで入力) | - | +| `--description` | プロジェクトの説明 (省略時はプロンプトで入力) | - | | `--package-manager` | 使用するパッケージマネージャー (pip, uv, pdm, poetry) | uv | +| `--dry-run` | 何も書き込まずに、作成されるファイルとパッケージを表示 | off | +| `--no-venv` | 仮想環境の作成をスキップ (`--no-install` を含意) | off | +| `--no-install` | 依存関係のインストールをスキップ | off | +| `--yes` / `-y` | プロジェクトをその場にデプロイするとき、既存ファイルを上書きする前の確認を行わない | off | | `--help` | コマンドのヘルプを表示 | - | +#### 設定ファイル (`--config` / `--save-config`) + +対話型セッションの回答は単純なマッピングであり、そのマッピングはファイルとして持ち運べます。これにより、 +プロジェクトの作成をコミットし、レビューし、再現できるものにできます。 + +```console +# 対話型セッションを記録する +$ fastkit init --interactive --save-config fastkit.config.json + +# 後で、プロンプトを一切出さずに再現する +$ fastkit init --config fastkit.config.json +``` + +`--save-config` を渡さなかった場合、最後まで進んだ対話型の実行では、選択内容を保存するかどうかと保存先を +尋ねられます。このプロンプトは人間がターミナルの前にいる場合にだけ表示されます — パイプ経由や CI での実行 +では何も表示されず、代わりにフラグでオプトインします。 + +**サポートされる形式** はファイルの拡張子で決まります: + +| 拡張子 | 利用可否 | +|---|---| +| `.json` | 常に利用可能 (標準ライブラリ) | +| `.toml` | 常に利用可能 (標準ライブラリ) | +| `.yaml` / `.yml` | PyYAML がインストールされている場合のみ — fastkit はこれをランタイム依存関係として追加しません | + +読み込まれるファイルは、トップレベルにマッピングを持ち、少なくとも `project_name`、`author`、`author_email` +を含んでいる必要があります。その後、対話型プロンプトと同じルール (プロジェクト名の形式、メールアドレスの +形式、`all_dependencies` がリストであること) で検証され、何かを書き込む前にすべての問題が報告されます。 +機能の選択だけを列挙し、`all_dependencies` を含まないファイルは対話型ビルダーを通じて展開されるため、 +手書きの設定とウィザードのセッションは同じパッケージセットに解決されます。 + +最小限の例: + +```json +{ + "project_name": "orders-api", + "author": "Developer Kim", + "author_email": "developer@example.com", + "description": "Domain-oriented orders service", + "architecture_preset": "domain-starter", + "package_manager": "uv" +} +``` + +#### 設定ファイルのスキーマ + +すべての設定ファイル — 手書きのものも `--save-config` で生成されたものも — は、ジェネレーターに渡される前に +同じノーマライザー (`backend/project_builder/config_schema.py` の `normalize_project_config`) を通ります。 +未知のキー、既知の項目に対する未知の選択肢、型の誤った値は、黙って無視されるのではなく、問題のある名前と +実際に許可されている値の一覧とともに拒否されます。 + +| キー | 形式 | 備考 | +|---|---|---| +| `project_name`、`author`、`author_email` | 文字列 | 必須 | +| `description` | 文字列 | 必須 | +| `architecture_preset` (別名 `preset`) | `"minimal"` \| `"single-module"` \| `"classic-layered"` \| `"domain-starter"` | 両方の表記がある場合は一致している必要があります | +| `package_manager` | `"pip"` \| `"uv"` \| `"pdm"` \| `"poetry"` | | +| `database` | 文字列、または `{"type": }` | `PostgreSQL`、`MySQL`、`MongoDB`、`Redis`、`SQLite`、`None` | +| `authentication`、`async_tasks`、`testing`、`caching`、`monitoring`、`migrations`、`logging` | 文字列、または `{"type": }` | 単一選択の項目 — 各項目の選択肢は上記の [機能カタログ](#interactive-builder-feature-catalog-interactive) を参照 | +| `utilities`、`tooling` | 文字列のリスト | 複数選択の項目 — 選択肢は同じカタログを参照 | +| `deployment` | 文字列のリスト | `Docker`、`docker-compose` の部分集合。`docker-compose` を選ぶと `Docker` も自動的に含まれます | +| `custom_packages` | 文字列のリスト | そのままインストールされる追加パッケージ | + +どの項目も、素の選択肢 (`"authentication": "JWT"`) と、`database` やプロジェクト自身の `[tool.fastapi-fastkit]` +メタデータが使う `{"type": ...}` のマッピング形式 (`"authentication": {"type": "JWT"}`) のどちらでも受け付けます +— どちらも同じ正規形に正規化され、`--save-config` は常に正規形である素の文字列の形式で書き出します。 + +ほとんどの項目を使った設定の例: + +```json +{ + "project_name": "orders-api", + "author": "Developer Kim", + "author_email": "developer@example.com", + "description": "Domain-oriented orders service", + "architecture_preset": "domain-starter", + "package_manager": "uv", + "database": "PostgreSQL", + "authentication": "JWT", + "async_tasks": "Celery", + "caching": "Redis", + "migrations": "Alembic", + "tooling": ["ruff", "makefile"], + "deployment": ["Docker"] +} +``` + +同じ設定を TOML で書いた場合: + +```toml +project_name = "orders-api" +author = "Developer Kim" +author_email = "developer@example.com" +description = "Domain-oriented orders service" +architecture_preset = "domain-starter" +package_manager = "uv" +database = "PostgreSQL" +authentication = "JWT" +async_tasks = "Celery" +caching = "Redis" +migrations = "Alembic" +tooling = ["ruff", "makefile"] +deployment = ["Docker"] +``` + +#### プレビューとステップのスキップ + +`--dry-run` は、作成されるツリーとインストールされるパッケージを — それをインストールするパッケージ +マネージャーとともに — 表示し、ディスクに一切触れずに終了します: + +```console +$ fastkit init --config fastkit.config.json --dry-run +``` + +`--no-install` は仮想環境を作成した時点で停止します。`--no-venv` は仮想環境の作成も (したがって +インストールも) スキップします。どちらもコンテナ内、CI、そして環境が fastkit の外で管理されている +あらゆる場面で役立ちます。 + +#### その場へのデプロイと上書きの確認 (`--yes` / `-y`) { #in-place-deployment-and-overwrite-confirmation-yes-y } + +プロジェクトをその場に — 新しいプロジェクトフォルダではなく現在のワークスペースに — デプロイする場合、 +fastkit は既存のファイルを置き換える前に「Overwrite these files?」と尋ねます。`--yes` (または短縮形の `-y`) +はこの確認をスキップして上書きを進めます。別に表示される「Do you want to proceed with project creation?」の +プロンプトには影響せず、こちらは引き続き表示されます。非対話型の環境 (CI やパイプ経由の実行のように stdin +が TTY でない場合) では、`--yes` を指定しなくても上書きの確認は自動的にスキップされます。 + #### 対話型プロンプト `init` コマンドは以下を尋ねます: @@ -96,6 +233,34 @@ $ fastkit init [OPTIONS] - `redis` - インメモリデータストア - `celery` - 分散タスクキュー +#### 対話型ビルダーの機能カタログ (`--interactive`) { #interactive-builder-feature-catalog-interactive } + +`fastkit init --interactive` は、上記の minimal/standard/full スタックではなく、項目ごとに構成された別の +カタログを順に進みます (情報源: `core/settings.py` の `FastkitConfig.PACKAGE_CATALOG`、および各選択肢が何を +生成するかについては `DynamicConfigGenerator.build_artifacts()`)。どの項目もデフォルトは `None` (スキップ) +で、`utilities` と `tooling` は複数選択です。 + +現在では、どの選択もパッケージをインストールするだけでなく実際のコードを生成します — `minimal` / +`single-module` (`main.py` を再生成) と `classic-layered` / `domain-starter` (`main.py` をそのまま残し、 +手動配線の警告を出す) の違いについては +[アーキテクチャプリセット / 機能マトリクス](../reference/preset-feature-matrix.md) を参照してください。 + +| 項目 | 選択肢 | インストールされるパッケージ | 生成されるファイル | +|---|---|---|---| +| `database` | PostgreSQL, MySQL, MongoDB, Redis, SQLite, None | PostgreSQL: `asyncpg`, `sqlalchemy` · MySQL: `aiomysql`, `sqlalchemy` · MongoDB: `motor` · Redis: `redis[hiredis]` · SQLite: `sqlalchemy`, `aiosqlite` | プリセットのパスに置かれるデータベース設定モジュール (例: `src/config/database.py`) | +| `authentication` | JWT, OAuth2, FastAPI-Users, Session-based, None | JWT: `pyjwt[crypto]`, `pwdlib[argon2]` · OAuth2: `authlib`, `itsdangerous`, `httpx` · FastAPI-Users: `fastapi-users[sqlalchemy]`, `pyjwt[crypto]`, `pwdlib[argon2]` · Session-based: `itsdangerous` | プリセットのパスに置かれる認証設定モジュール。OAuth2 とセッションベースは `main.py` にミドルウェアのセットアップも追加 | +| `async_tasks` | Celery, Dramatiq, None | Celery: `celery[redis]`, `redis[hiredis]` · Dramatiq: `dramatiq[redis]`, `redis[hiredis]` | `/worker.py` (バックグラウンドワーカー) + `/features/tasks.py` (タスク用ルート) | +| `testing` | Basic, Coverage, Advanced, None | Basic: `pytest`, `pytest-asyncio`, `httpx` · Coverage: + `pytest-cov` · Advanced: + `faker`, `factory-boy` | `pytest.ini`。Advanced では `tests/factories.py` と `tests/test_factories.py` も追加 | +| `caching` | Redis, None | Redis: `redis[hiredis]`, `fastapi-cache2`, `jinja2` | `/features/cache.py` (キャッシュされるエンドポイント) | +| `monitoring` | Loguru, OpenTelemetry, Prometheus, None | Loguru: `loguru` · OpenTelemetry: `opentelemetry-api`, `opentelemetry-sdk`, `opentelemetry-instrumentation-fastapi`, `opentelemetry-exporter-otlp-proto-http` · Prometheus: `prometheus-client`, `prometheus-fastapi-instrumentator` | 独立したファイルはなし — `main.py` に直接配線 (import / lifespan / setup) | +| `utilities` (複数選択) | CORS, Rate-Limiting, Pagination, WebSocket, None | CORS: なし (FastAPI に組み込み) · Rate-Limiting: `slowapi` · Pagination: `fastapi-pagination` · WebSocket: `websockets` | CORS と Rate-Limiting は `main.py` に直接配線。Pagination は `/features/pagination.py`、WebSocket は `/features/websocket.py` を追加 | +| `migrations` | Alembic, None | Alembic: `alembic` | `alembic.ini`、`alembic/env.py`、`alembic/script.py.mako`、ベースラインの `alembic/versions/_initial.py`、`scripts/migrate.sh` — database の項目が SQL データベースの場合のみ | +| `tooling` (複数選択) | ruff, pre-commit, github-actions, devcontainer, makefile, None | ruff: `ruff` · pre-commit: `pre-commit` · github-actions / devcontainer / makefile: なし | ruff は `pyproject.toml` に `[tool.ruff]` ブロックをマージ (`pip` プロジェクトでは `ruff.toml` を作成)。pre-commit は `.pre-commit-config.yaml`、github-actions は `.github/workflows/test.yml`、devcontainer は `.devcontainer/devcontainer.json`、makefile は `Makefile` を作成 | +| `logging` | structured, None | なし (標準ライブラリの `logging` + `json` のみ) | `/logging_config.py` (構造化 JSON ロギング + request-id ミドルウェア)。`main.py` に配線 | + +選択内容にかかわらず、生成されるすべてのプロジェクトの `main.py` には `/health` と `/ready` エンドポイントが +含まれます。また `--dry-run` は、何かを書き込む前にこのファイルセットをそのまま表示します。 + #### 例
@@ -227,6 +392,31 @@ $ fastkit addroute users my-api | `PUT` | `/api/v1/users/{user_id}` | ユーザーを更新 | | `DELETE` | `/api/v1/users/{user_id}` | ユーザーを削除 | +#### コードが挿入される場所 + +`addroute` はプロジェクトから 2 つの情報を必要とします: 新しい import を置く場所と、ルーターを登録する場所です。 +テンプレートと生成されたエントリポイントは、その両方をアンカーコメントで示しています: + +```python +# src/app/api/router.py +from src.app.api import health + +# fastkit:imports + +api_router = APIRouter() +api_router.include_router(health.router) + +# fastkit:routes +``` + +ファイルを編集するときは、これらのコメントをそのまま残してください — `addroute` は `# fastkit:imports` と +`# fastkit:routes` の直上に挿入します。アンカーを失ったプロジェクト (またはアンカーが導入される前に生成された +プロジェクト) でも動作はします: fastkit は AST ベースの挿入にフォールバックし、import ブロックとルーター登録の +位置を自分で探します。アンカーは結果を予測可能にするためのものです。 + +`addroute` はエントリポイントを特定するためにプロジェクトの `[tool.fastapi-fastkit]` ブロックも読み取るため、 +ディスク上のスキャンだけでは誤って順位付けされてしまうレイアウトでも動作します。 + ### `startdemo` あらかじめ用意されたテンプレートから FastAPI プロジェクトを作成します。 @@ -241,9 +431,23 @@ $ fastkit startdemo [OPTIONS] | オプション | 説明 | デフォルト | |---|---|---| +| `--project-name` | プロジェクト名 (省略時はプロンプトで入力) | - | +| `--author` | 作者名 (省略時はプロンプトで入力) | - | +| `--author-email` | 作者のメールアドレス (省略時はプロンプトで入力) | - | +| `--description` | プロジェクトの説明 (省略時はプロンプトで入力) | - | | `--package-manager` | 使用するパッケージマネージャー (pip, uv, pdm, poetry) | uv | +| `--dry-run` | 何も書き込まずに、作成されるファイルとパッケージを表示 | off | +| `--no-venv` | 仮想環境の作成をスキップ (`--no-install` を含意) | off | +| `--no-install` | 依存関係のインストールをスキップ | off | +| `--yes` / `-y` | プロジェクトをその場にデプロイするとき、既存ファイルを上書きする前の確認を行わない | off | | `--help` | コマンドのヘルプを表示 | - | +`init` と同様に、`startdemo` は対象のプロジェクトディレクトリがすでに存在する場合は実行を拒否します +(`Error: Project '{name}' already exists.`)。`--dry-run` は例外です: ディスクに一切書き込まないため、 +すでにディレクトリが存在する名前を対象にすることも許可されています。現在のワークスペースにデプロイする +ときに `--yes` が何をするかは、上記の +[その場へのデプロイと上書きの確認](#in-place-deployment-and-overwrite-confirmation-yes-y) を参照してください。 + #### 対話型プロンプト `startdemo` コマンドは以下を尋ねます: @@ -259,11 +463,20 @@ $ fastkit startdemo [OPTIONS] | テンプレート | 説明 | 機能 | |---|---|---| | `fastapi-default` | シンプルな FastAPI プロジェクト | 基本的な CRUD、モックデータ | -| `fastapi-async-crud` | 非同期アイテム管理 API | async/await、パフォーマンス重視 | +| `fastapi-domain-starter` | ドメイン指向のスターター | ビジネス概念ごとに 1 フォルダ、`/health` | +| `fastapi-auth-jwt` | JWT 認証 | アクセス/リフレッシュトークンのローテーション、argon2id、ロールとスコープ、Alembic | +| `fastapi-sqlmodel` | 非同期 SQLModel 永続化 | SQLModel + 非同期 SQLAlchemy、Alembic、汎用 CRUD、ページネーション | +| `fastapi-llm-agent` | ストリーミング Claude エージェント | SSE ストリーミング、ツール呼び出しループ、会話メモリ | | `fastapi-custom-response` | カスタムレスポンス機構 | カスタムレスポンス、ページネーション | -| `fastapi-dockerized` | Docker 化された FastAPI API | Docker、本番運用対応 | | `fastapi-psql-orm` | PostgreSQL 連携 FastAPI API | PostgreSQL、SQLAlchemy、Alembic | +| `fastapi-mcp` | Model Context Protocol サーバー | MCP 統合 | +| `fastapi-single-module` | 単一ファイルのサンドボックス | 1 モジュール、パッケージ境界なし | | `fastapi-empty` | 最小構成 FastAPI プロジェクト | 必要最低限のセットアップ | +| `fastapi-async-crud` | *(非推奨)* 非同期アイテム管理 API | `fastapi-sqlmodel` に置き換え | +| `fastapi-dockerized` | *(非推奨)* Docker 化された FastAPI API | Docker 関連のツールは新しいテンプレートに同梱 | + +非推奨のテンプレートも引き続き動作するプロジェクトを生成します。単に、推奨される出発点ではなくなったと +いうだけです。[どのスターターを選ぶべき?](choosing-a-starter.md) を参照してください。 #### 例 @@ -331,9 +544,13 @@ INFO: Uvicorn running on http://127.0.0.1:8000 #### 動作要件 - FastAPI プロジェクトのディレクトリで実行する必要がある -- プロジェクトに FastAPI アプリを含む `src/main.py` が存在する必要がある +- プロジェクトが FastAPI アプリを公開している必要がある — `[tool.fastapi-fastkit].app_module` があればそこから + 解決され、なければ `main.py` をスキャンして探索される - 仮想環境を有効化しておくこと +記録されたエントリポイントでもスキャンでもモジュールが見つからない場合、`runserver` は `main.py` が見つから +なかったことを報告して停止します。 + ### `list-templates` 利用可能なすべての FastAPI プロジェクトテンプレートを一覧表示します。 @@ -372,6 +589,40 @@ $ fastkit list-templates
+## プロジェクトのメタデータ (`[tool.fastapi-fastkit]`) + +fastkit が生成するすべてのプロジェクトは、どのように生成されたかを自身の `pyproject.toml` に記録します: + +```toml +[tool.fastapi-fastkit] +managed = true +version = "1.4.0" +template = "fastapi-domain-starter" +preset = "domain-starter" +package_manager = "uv" +app_module = "src.app.main:app" +features = ["database:PostgreSQL", "authentication:JWT"] +``` + +| キー | 意味 | +|---|---| +| `managed` | 常に `true`。プロジェクトが fastkit 管理下であることを示す | +| `version` | プロジェクトを生成した fastkit のバージョン | +| `template` | `startdemo` のテンプレート、またはプリセットのベーステンプレート | +| `preset` | `init --interactive` のアーキテクチャプリセット (それ以外の場合は省略) | +| `package_manager` | 環境の構築に使われたパッケージマネージャー | +| `app_module` | uvicorn の `module:attr` 形式のエントリポイント | +| `features` | 対話型での選択を `":"` 形式で並べたフラットなリスト | + +このブロックは、プロジェクトのエントリポイントに関する唯一の情報源です: `fastkit runserver` はツリーの +スキャンにフォールバックする前にここから `app_module` を読み取り、`fastkit addroute` はアプリケーションの +特定にこれを使い、生成される Dockerfile も `CMD` で同じ値を使います。 + +このブロックは生成の最後に書き込まれます — パッケージマネージャーは依存関係を解決する際に `pyproject.toml` +を書き換えるため、その後に記録する必要があるからです — また、後で再生成した場合は 2 つ目のコピーを追加する +のではなく置き換えます。自分でレイアウトを再編成したことを fastkit に伝える方法としては、`app_module` を +手で編集するのが正式にサポートされた方法です。 + ## 環境変数 FastAPI-fastkit は次の環境変数を尊重します: @@ -381,6 +632,17 @@ FastAPI-fastkit は次の環境変数を尊重します: | `FASTKIT_CONFIG_DIR` | 設定ディレクトリ | `~/.fastkit` | | `FASTKIT_TEMPLATES_DIR` | カスタムテンプレートのディレクトリ | 組み込みテンプレート | | `FASTKIT_LOG_LEVEL` | ロギングレベル | `INFO` | +| `FASTKIT_SUBPROCESS_TIMEOUT` | すべてのパッケージマネージャーのサブプロセスのタイムアウトを秒単位で上書き | 操作ごと (30 / 120 / 900) | + +パッケージマネージャーの呼び出しにはそれぞれ上限が設けられているため、ハングした子プロセスが CLI をブロック +し続けることはありません: 利用可否のチェックは 30 秒、仮想環境の作成は 120 秒、依存関係のインストールは +900 秒です。`FASTKIT_SUBPROCESS_TIMEOUT` はこの 3 つすべてを 1 つの値で置き換えます — 遅いネットワーク上で、 +あるいは CI で早めに失敗させたいときに便利です。正の整数でない値は無視されます。 + +```console +$ export FASTKIT_SUBPROCESS_TIMEOUT=1800 +$ fastkit startdemo fastapi-sqlmodel +``` ### 例 diff --git a/docs/ja/user-guide/creating-projects.md b/docs/ja/user-guide/creating-projects.md index 1ba98eb..5a796f5 100644 --- a/docs/ja/user-guide/creating-projects.md +++ b/docs/ja/user-guide/creating-projects.md @@ -462,7 +462,7 @@ settings = Settings()
```console -$ pip install requests httpx python-jose +$ pip install requests httpx pyjwt $ pip freeze > requirements.txt ``` diff --git a/docs/ko/contributing/development-setup.md b/docs/ko/contributing/development-setup.md index eae05ff..bf404d8 100644 --- a/docs/ko/contributing/development-setup.md +++ b/docs/ko/contributing/development-setup.md @@ -212,6 +212,49 @@ Success: no issues found in 12 source files | `make inspect-templates-verbose` | verbose 출력으로 템플릿 검사 | | `make inspect-template` | 특정 템플릿 검사 (TEMPLATES 파라미터) | +작업을 반복하는 동안 알아 두면 좋은 플래그가 기반 스크립트에 세 가지 있습니다. +`--offline`은 PyPI 최신 버전 조회를 건너뛰고, `--no-smoke`는 가장 느린 단계인 +uvicorn으로 생성된 프로젝트를 띄우는 과정을 건너뛰며, `--mypy`는 생성된 +프로젝트의 타입을 검사합니다(느리기 때문에 명시적으로 켜야 합니다). 각 검사가 +실제로 무엇을 하는지는 [템플릿 품질 보증](../reference/template-quality-assurance.md)을 +참고하세요. + +```console +$ python scripts/inspect-templates.py --templates fastapi-sqlmodel --offline --no-smoke +``` + +### 생성되는 코드는 Jinja2 fragment에 있습니다 + +`fastkit init --interactive`가 프로젝트에 쓰는 모든 파일(`main.py`, 데이터베이스와 +인증 모듈, Docker 파일, pytest 설정)은 Python 코드 안에서 문자열을 이어 붙여 +만드는 것이 아니라 `src/fastapi_fastkit/fragments/` 아래의 템플릿에서 렌더링됩니다. +`main.py.j2`는 어떤 기능도 하드코딩하지 않습니다. 이 템플릿은 +`DynamicConfigGenerator`가 제공하는 `import_fragments`, `lifespan_fragments`, +`setup_fragments` 목록을 순회하면서 각 fragment를 include하므로, 기능들을 +자유롭게 조합할 수 있습니다. + +기능을 추가하는 과정은 대략 다음과 같습니다: + +1. 코드 조각을 `main/imports/`, `main/lifespan/`, `main/setup/` 중 필요한 곳에 + 넣습니다(lifespan fragment는 공백 네 칸으로 들여쓰고, 나머지 두 종류는 + 들여쓰지 않습니다). +2. 관련 설정 조회 조건으로 감싸서 `DynamicConfigGenerator._main_fragments()`에 + 등록합니다. +3. 독립된 모듈이 필요하다면 `/.py.j2`를 추가하고 + `self._render(...)`로 렌더링합니다. +4. `tests/test_backends/test_project_builder_config_generator.py`의 조합 + 매트릭스를 확장합니다. 이 테스트는 렌더링된 모든 Python 파일이 + `ast.parse`로 파싱되는지 검증합니다. + +놓치기 쉬운 제약이 두 가지 있습니다. 모든 fragment는 끝에 줄바꿈이 있어야 +하고(환경이 `keep_trailing_newline=True`로 만들어지기 때문입니다), +`main.py.j2`는 `# fastkit:imports` / `# fastkit:routes` 앵커를 유지해야 합니다. +`fastkit addroute`가 이 앵커 위치에 코드를 삽입하기 때문입니다. + +공식 절차는 fragment 옆에 있는 +[`src/fastapi_fastkit/fragments/README.md`](https://github.com/bnbong/FastAPI-fastkit/blob/main/src/fastapi_fastkit/fragments/README.md)에 +정리되어 있습니다. + ### 문서 명령 | 명령 | 설명 | @@ -353,13 +396,16 @@ FastAPI-fastkit/ - **`src/fastapi_fastkit/`** — 메인 패키지 소스 코드 - **`cli.py`** — 메인 CLI 진입점 - **`backend/`** — 핵심 백엔드 로직 - - **`inspector.py`** — 템플릿 inspector + - **`inspection/`** — 템플릿 검사 파이프라인 (context, checks, consistency, freshness, smoke, lint, report) + - **`inspector.py`** — `inspection/` 위에 둔 퍼사드로, 기존 import 경로를 유지하기 위해 남겨 둠 - **`interactive/`** — 대화형 모드 컴포넌트 (prompts, selectors, validators) - **`package_managers/`** — 패키지 매니저 구현 (pip, uv, pdm, poetry) - - **`project_builder/`** — 프로젝트 빌드 유틸리티 + - **`project_builder/`** — 프로젝트 빌드 유틸리티 (fragment 기반 설정 생성기 포함) + - **`scaffolder.py`** — `init`과 `startdemo`가 공유하는 생성 파이프라인 - **`transducer.py`** — 템플릿 transducer - **`core/`** — 핵심 설정과 예외 - - **`fastapi_project_template/`** — 프로젝트 템플릿 (fastapi-default, fastapi-async-crud 등) + - **`fragments/`** — 생성된 프로젝트에 렌더링되는 Jinja2 fragment (위 내용 참고) + - **`fastapi_project_template/`** — 프로젝트 템플릿 (fastapi-default, fastapi-domain-starter 등) - **`utils/`** — 공유 유틸리티 함수 - **`tests/`** — 테스트 스위트 - **`test_backends/`** — 백엔드 관련 테스트 diff --git a/docs/ko/contributing/template-creation-guide.md b/docs/ko/contributing/template-creation-guide.md index 8e8bcbb..4834c1e 100644 --- a/docs/ko/contributing/template-creation-guide.md +++ b/docs/ko/contributing/template-creation-guide.md @@ -97,8 +97,7 @@ fastapi-{template-name}/ │ ├── lint.sh-tpl # 린팅 │ ├── run-server.sh-tpl # 서버 실행 │ └── test.sh-tpl # 테스트 실행 -├── pyproject.toml-tpl # ✅ 주요 메타데이터 (PEP 621, 권장) -├── setup.py-tpl # 🟡 Legacy 메타데이터 (하위 호환을 위해 허용) +├── pyproject.toml-tpl # ✅ 주요 메타데이터 (PEP 621, 필수) ├── requirements.txt-tpl # 🟡 pyproject 가 의존성을 선언하면 선택 사항 ├── setup.cfg-tpl # 개발 도구 설정 ├── README.md-tpl # ✅ 프로젝트 문서 (필수) @@ -110,10 +109,16 @@ fastapi-{template-name}/ - `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` (PEP 621) +- `pyproject.toml-tpl` 의 `[project].dependencies` 또는 `requirements.txt-tpl` 에 `fastapi` 의존성 선언 -`pyproject.toml-tpl`이 `[project].dependencies`를 선언한다면 `requirements.txt-tpl`은 더 이상 필수가 아닙니다. 최신 템플릿이라면 `pyproject.toml-tpl`을 주요 메타데이터 파일로 사용하는 편을 권장합니다. +`pyproject.toml-tpl`이 `[project].dependencies`를 선언한다면 `requirements.txt-tpl`은 필수가 아닙니다. 다만 템플릿이 두 파일을 모두 제공한다면 내용이 서로 일치해야 합니다. 두 파일 사이에 의존성 불일치가 있으면 검사가 실패합니다. + +**새 템플릿은 pyproject를 우선해야 하며 `setup.py-tpl`을 제공하면 안 됩니다.** v1.4.0 기준으로 번들 템플릿 중 이 파일을 가진 템플릿은 없습니다. 저장소 밖의 템플릿이 계속 동작하도록 검사 단계에서는 여전히 이 파일을 허용하지만, 이는 레거시 경로입니다. + +**Python 3.12를 일관되게 대상으로 하세요**: `requires-python = ">=3.12"`, black `target-version = ["py312"]`, mypy `python_version = "3.12"`, 그리고 Docker 베이스 이미지는 `python:3.12-slim`을 사용합니다. 검사 단계에서는 세 가지 고정 값이 서로 일치하는지 확인합니다. + +**생성된 프로젝트의 런타임 의존성으로 `FastAPI-fastkit`을 절대 선언하지 마세요.** CLI는 개발 도구입니다. 템플릿의 의존성 목록에는 생성된 애플리케이션이 실제로 import하는 패키지만 들어 있어야 합니다. ### 파일 작성 가이드 @@ -237,52 +242,23 @@ isort==5.12.0 mypy==1.7.1 ``` -#### 4. setup.py-tpl 작성 (레거시 — `pyproject`가 있다면 선택 사항) +#### 4. 라우트 앵커 -Legacy 템플릿을 위해 유지됩니다. 새 템플릿은 `pyproject.toml-tpl` 을 제공한다면 이 파일이 필요 없습니다. +`main.py-tpl`이나 API 집계 모듈을 제공하는 템플릿은 `fastkit addroute`가 코드를 삽입할 위치를 표시해 두어야 합니다: ```python -""" - package setup +from src.app.api import health -Project created with FastAPI-fastkit. -""" -from setuptools import find_packages, setup - -# 의존성 목록 (타입 어노테이션 필수) -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", -] +# fastkit:imports -setup( - name="", - version="1.0.0", - description="[FastAPI-fastkit templated] ", # 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", - ], -) +api_router = APIRouter() +api_router.include_router(health.router) + +# fastkit:routes ``` +앵커가 없으면 `addroute`는 AST 기반 삽입 방식으로 대신 동작해 import 블록과 라우터 등록 위치를 직접 찾습니다. 따라서 앵커가 없는 템플릿도 동작은 하지만, 결과를 예측하기가 더 어렵습니다. 이 파일들을 수정할 때는 앵커 주석을 그대로 유지하세요. + #### 5. 테스트 파일 작성 ```python @@ -342,8 +318,17 @@ make inspect-template TEMPLATES="fastapi-your-template" # verbose 출력으로 검증 python scripts/inspect-templates.py --templates "fastapi-your-template" --verbose + +# 빠른 로컬 반복 실행: 네트워크 조회 없음, 서버 실행 없음 +python scripts/inspect-templates.py --templates "fastapi-your-template" \ + --offline --no-smoke + +# PR을 열기 전 전체 검사 (생성된 프로젝트에 mypy 검사 추가) +python scripts/inspect-templates.py --templates "fastapi-your-template" --mypy ``` +`--offline`은 PyPI 의존성 최신성 조회를 건너뛰고, `--no-smoke`는 가장 느린 단계인 생성된 프로젝트 실행을 건너뛰며, `--mypy`는 생성된 프로젝트의 타입을 검사합니다. 각 검사가 하는 일은 [템플릿 품질 보증](../reference/template-quality-assurance.md)을 참고하세요. + !!! note PR을 제출하면 **Template PR Inspection** 워크플로가 자동으로 실행되어 템플릿 변경 사항을 검증합니다. 검증 결과는 PR에 직접 피드백으로 남습니다. @@ -356,7 +341,7 @@ inspector 가 자동으로 검증하는 항목들입니다: - [ ] `tests/` 디렉터리 존재 - [ ] `README.md-tpl` 파일 존재 -- [ ] `pyproject.toml-tpl` (권장) 또는 `setup.py-tpl` (레거시) 중 최소 하나 존재 +- [ ] `pyproject.toml-tpl` 존재 (새 템플릿에는 `setup.py-tpl` 없음) #### ✅ 파일 확장자 검증 @@ -368,7 +353,8 @@ inspector 가 자동으로 검증하는 항목들입니다: - [ ] 다음 중 최소 한 곳에 `fastapi` 가 선언됨: - [ ] `pyproject.toml-tpl` 의 `[project].dependencies` (권장) - [ ] `requirements.txt-tpl` - - [ ] `setup.py-tpl` 의 `install_requires` +- [ ] `requirements.txt-tpl` 과 `[project].dependencies` 의 내용이 서로 어긋나지 않음 +- [ ] `FastAPI-fastkit` 이 런타임 의존성이 **아님** #### ✅ FastAPI 구현 검증 @@ -381,6 +367,24 @@ inspector 가 자동으로 검증하는 항목들입니다: - [ ] 의존성 설치 성공 - [ ] 모든 pytest 테스트 통과 +#### ✅ 스모크 테스트 + +- [ ] 생성된 프로젝트가 uvicorn으로 실행됨 +- [ ] `/docs` 가 응답함 +- [ ] `/health` 가 응답함 + +uvicorn 대상은 생성된 `pyproject.toml`의 `[tool.fastapi-fastkit].app_module`, `template-config.yml`의 `app_module`, 관례적인 `main.py` 위치 순서로 결정됩니다. + +#### ✅ 설정 일관성 + +- [ ] `requires-python`, black `target-version`, mypy `python_version` 이 모두 Python 3.12를 가리킴 +- [ ] `requirements.txt-tpl` 과 `[project].dependencies` 사이에 의존성 불일치가 없음 +- [ ] `FastAPI-fastkit` 에 대한 자기 의존성이 없음 + +#### ✅ 플레이스홀더 잔여 확인 + +- [ ] 프로젝트 생성 후 남아 있는 ``가 없음 + #### ✅ 자동 템플릿 테스트 FastAPI-fastkit 은 모든 템플릿에 대해 종합 테스트를 실행하는 **자동 템플릿 테스트** 시스템을 포함합니다: diff --git a/docs/ko/reference/preset-feature-matrix.md b/docs/ko/reference/preset-feature-matrix.md index d6b13a8..1f2ba3f 100644 --- a/docs/ko/reference/preset-feature-matrix.md +++ b/docs/ko/reference/preset-feature-matrix.md @@ -29,12 +29,18 @@ | 기능 | `minimal` / `single-module` | `classic-layered` / `domain-starter` | |---|---|---| | **데이터베이스** (PostgreSQL, MySQL, SQLite, MongoDB) | 설정 모듈을 생성하고, 추가로 다시 생성된 `main.py`에 `await init_db()` 호출용 스텁을 넣어 줍니다. | 설정 모듈을 프리셋에 맞는 경로에 생성합니다. 템플릿이 제공하는 `main.py`는 **그대로 유지**되므로, `get_db()`를 라우터에 직접 연결해야 합니다. | -| **인증** (JWT, FastAPI-Users, OAuth2, Session-based) | 인증 설정 모듈을 생성합니다. JWT의 경우 다시 생성된 `main.py`에 `HTTPBearer` import까지 추가합니다. | 인증 설정 모듈을 프리셋에 맞는 경로에 생성합니다. `main.py`에는 import가 추가되지 않으므로 의존성을 직접 연결해야 합니다. | -| **백그라운드 작업** (Celery, Dramatiq) | 패키지가 설치됩니다. 현재 main.py 오버레이는 없습니다. | 동일. | -| **캐싱** (Redis) | 패키지가 설치됩니다. 현재 main.py 오버레이는 없습니다. | 동일. | +| **인증** (JWT, FastAPI-Users, OAuth2, Session-based) | 인증 설정 모듈을 생성합니다. JWT는 `HTTPBearer`를 import하고, OAuth2와 Session-based는 `main.py` 미들웨어 설정도 추가합니다. | 인증 설정 모듈을 프리셋에 맞는 경로에 생성합니다. `main.py`에는 import가 추가되지 않으므로 의존성을 직접 연결해야 합니다. | +| **백그라운드 작업** (Celery, Dramatiq) | `worker.py`와 `features/tasks.py`를 생성하고, 다시 생성된 `main.py`에 작업 라우터를 마운트합니다. | `worker.py`와 `features/tasks.py`를 생성하지만, 템플릿이 제공하는 `main.py`는 **그대로 유지**됩니다. 라우터는 직접 마운트해야 합니다. | +| **캐싱** (Redis) | `features/cache.py`를 생성하고, 다시 생성된 `main.py`에서 이를 마운트하고 초기화합니다. | `features/cache.py`를 생성합니다. 마운트는 직접 해야 합니다. | +| **유틸리티: Pagination / WebSocket** | `features/pagination.py` / `features/websocket.py`를 생성하고, 다시 생성된 `main.py`에 라우터를 마운트합니다. | 같은 모듈을 생성합니다. 마운트는 직접 해야 합니다. | +| **모니터링: OpenTelemetry** | 별도 파일은 없습니다. 트레이싱 설정이 다시 생성된 `main.py`에 직접 추가됩니다. | 별도 파일은 없습니다. 템플릿이 제공하는 `main.py`가 유지되므로 트레이싱이 **연결되지 않습니다**. 설정을 직접 추가해야 합니다. | +| **마이그레이션** (Alembic) | SQL 데이터베이스를 선택하면 `main.py` 오버레이와 관계없이 `alembic.ini`, `alembic/env.py`, 기준(baseline) 리비전, `scripts/migrate.sh`를 생성합니다. | 동일. Alembic 파일 생성은 `main.py`가 유지되는지 여부와 관계가 없습니다. | +| **도구** (ruff, pre-commit, github-actions, devcontainer, makefile) | `main.py` 오버레이와 관계없이 해당하는 설정 파일을 생성합니다. | 동일. | +| **로깅** (structured) | `logging_config.py`를 생성하고, 다시 생성된 `main.py`에 request-id 미들웨어를 연결합니다. | `logging_config.py`를 생성합니다. 템플릿이 제공하는 `main.py`가 유지되므로 미들웨어는 직접 연결해야 합니다. | | **CORS** (유틸리티) | 다시 생성된 `main.py`에 `allow_origins=['*']` 형태로 `CORSMiddleware`가 추가됩니다. | 템플릿이 제공하는 `main.py`에 **이미 연결**돼 있습니다 (`settings.all_cors_origins` 조건부). `.env`의 `BACKEND_CORS_ORIGINS` 값만 채우면 활성화되므로 코드를 수정할 필요가 없습니다. | -| **테스트** (Basic / Coverage / Advanced) | 프로젝트 루트에 `pytest.ini` 가 생성됩니다. | 동일. | +| **테스트** (Basic / Coverage / Advanced) | 프로젝트 루트에 `pytest.ini` 가 생성됩니다. Advanced는 `tests/factories.py`와 `tests/test_factories.py`도 추가합니다. | 동일. | | **배포** (Docker, docker-compose) | 프로젝트 루트에 `Dockerfile` 또는 `docker-compose.yml` 이 작성됩니다. | 동일. | +| **`/health` / `/ready`** | 다시 생성된 `main.py`에 항상 포함됩니다. | 그대로 유지되는, 템플릿이 제공하는 `main.py`에 이미 포함돼 있습니다. | ## "프리셋 호환성" 경고가 표시되는 시점 @@ -44,10 +50,15 @@ |---|---| | `CORS` (유틸리티) | ❌ — 템플릿이 제공하는 `main.py`에 이미 연결돼 있습니다. `.env`에 `BACKEND_CORS_ORIGINS`만 채우면 됩니다. | | `Rate-Limiting` (유틸리티) | ✅ — `slowapi` 리미터 설정이 추가되지 않음 | -| `Prometheus` (모니터링) | ✅ — `Instrumentator().instrument(app)` 호출이 추가되지 않음 | +| `Pagination` / `WebSocket` (유틸리티) | ✅ — 생성된 라우터(`features/pagination.py` / `features/websocket.py`)가 마운트되지 않음 | +| `Celery` / `Dramatiq` (async_tasks) | ✅ — 생성된 `worker.py` / `features/tasks.py` 라우터가 마운트되지 않음 | +| `Redis` (캐싱) | ✅ — 생성된 `features/cache.py`가 마운트되거나 초기화되지 않음 | +| `Prometheus` / `OpenTelemetry` (모니터링) | ✅ — 계측 설정이 `main.py`에 추가되지 않음 | +| `structured` (로깅) | ✅ — `logging_config.py`는 생성되지만, request-id 미들웨어가 `main.py`에 추가되지 않음 | +| `Alembic` (마이그레이션), `tooling` 선택 | ❌ — 이 파일들은 프리셋과 관계없이 생성되고 바로 사용할 수 있으며, `main.py`에 의존하지 않음 | | 모든 데이터베이스 / 인증 선택 | ⚠️ — 설정 파일은 생성되지만, 라우터에 `Depends()` 로 연결하는 작업은 직접 해야 함 | -`minimal` 과 `single-module` 프리셋에서는 동적 `main.py` 오버레이가 CORS, 레이트 제한, Prometheus 계측을 자동으로 처리하므로 경고가 발생하지 않습니다. +`minimal` 과 `single-module` 프리셋에서는 동적 `main.py` 오버레이가 위의 모든 항목을 자동으로 처리하므로 경고가 발생하지 않습니다. ## 지원되지 않는 조합 (안전 우선) diff --git a/docs/ko/reference/template-quality-assurance.md b/docs/ko/reference/template-quality-assurance.md index 9a8d3f5..e05f950 100644 --- a/docs/ko/reference/template-quality-assurance.md +++ b/docs/ko/reference/template-quality-assurance.md @@ -21,6 +21,66 @@ FastAPI-fastkit은 **서로 보완하는 두 가지 품질 보증 시스템**을 - ✅ **의존성 확인** — FastAPI 와 필수 의존성이 제대로 선언돼 있는지 확인 - ✅ **FastAPI 구현 확인** — 템플릿이 적절한 FastAPI 앱 초기화를 포함하는지 확인 - ✅ **테스트 실행** — 템플릿 테스트를 실행해 동작을 확인 +- ✅ **스모크 테스트** — 생성된 프로젝트를 uvicorn으로 띄우고 HTTP 엔드포인트를 확인 +- ✅ **설정 일관성** — Python 버전 고정, 의존성 불일치, 자기 자신에 대한 의존성 확인 +- ✅ **플레이스홀더 잔여 확인** — 프로젝트 생성 후 ``가 남아 있지 않은지 확인 +- ✅ **의존성 최신성** — 고정된 버전이 PyPI 최신 릴리스보다 뒤처져 있으면 경고 + +## 각 검사가 하는 일 + +검사는 템플릿에서 **실제 프로젝트**를 생성한 다음(복사, `-tpl` 변환, 메타데이터 +주입), 템플릿 원본이 아니라 그렇게 생성된 프로젝트를 대상으로 모든 검사를 +실행합니다. 템플릿만 읽는 검사로는 생성 과정에서 치환되지 않은 플레이스홀더를 +잡아낼 수 없기 때문입니다. + +### 스모크 테스트 + +생성된 프로젝트를 빈 포트에서 uvicorn으로 시작하고, inspector는 서버가 응답하거나 +제한 시간이 끝날 때까지 폴링합니다. 서버가 뜨면 `/docs`와 `/health`를 요청해 +상태 코드를 확인합니다. 그다음 서버 프로세스를 종료하며, 종료되지 않으면 +`SIGKILL`까지 단계적으로 보냅니다. + +uvicorn 대상은 다음 순서로 결정됩니다. 먼저 생성된 `pyproject.toml`의 +`[tool.fastapi-fastkit].app_module`을 확인하고, 다음으로 템플릿의 +`template-config.yml`에 있는 `app_module`을, 마지막으로 관례적인 `main.py` +위치를 확인합니다. 이는 `fastkit runserver`가 사용하는 계약과 같습니다. 따라서 +스모크 테스트에서 서버가 뜨는 템플릿이라면, 그 템플릿으로 생성한 프로젝트도 +실행됩니다. + +정적 분석으로는 잡을 수 없는 문제를 잡아내는 검사가 바로 이 검사입니다. 런타임에만 +해석되는 import, 예외를 던지는 `lifespan`, 기본값이 없는 설정 필드 같은 문제가 +여기에 해당합니다. + +### 설정 일관성 + +템플릿의 `pyproject.toml-tpl`을 기준으로 다음 세 가지를 검증합니다: + +- **Python 버전 고정 값이 서로 일치하고 3.12를 대상으로 하는지** — + `requires-python`, black의 `target-version`, mypy의 `python_version`을 + 확인합니다. `>=3.12`를 선언하면서 `py311`로 포매팅하는 템플릿은 언젠가 + 기여자에게 문제를 일으킬 버그입니다. +- **의존성 불일치가 없는지** — `requirements.txt-tpl`과 `[project].dependencies`에 + 선언된 패키지가 서로 달라서는 안 됩니다. 템플릿은 패키지 매니저 호환성을 위해 + 두 파일을 모두 제공하는데, 무언가가 검사하지 않으면 두 목록은 반드시 어긋나게 + 됩니다. +- **자기 자신에 대한 의존성이 없는지** — 생성된 프로젝트는 `FastAPI-fastkit`을 + 런타임 의존성으로 선언하면 안 됩니다. CLI는 개발 도구입니다. 생성되는 모든 앱에 + CLI를 포함하면 fastkit의 전체 의존성 트리가 관련 없는 프로젝트에까지 딸려 + 들어갑니다. + +### 플레이스홀더 잔여 확인 + +``, `` 같은 모든 플레이스홀더는 생성 과정에서 +치환되어야 합니다. 생성된 프로젝트에 ``가 하나라도 남아 있으면 +오류로 처리합니다. 메타데이터 주입 단계가 그 파일을 인식하지 못한다는 뜻이며, +사용자는 그 파일을 치환되지 않은 상태 그대로 받게 됩니다. + +### 의존성 최신성 + +고정된 버전을 PyPI의 최신 릴리스와 비교해 오류가 아니라 항상 경고로 보고합니다. +템플릿이 마이너 버전 하나만큼 뒤처져 있는 것은 실패가 아니라 참고 정보이기 +때문입니다. 네트워크에 접근하는 검사는 이것 하나뿐이며, `--offline`을 지정하면 +건너뜁니다. ## 자동 템플릿 테스트 시스템 @@ -148,6 +208,30 @@ $ python scripts/inspect-templates.py --verbose $ python scripts/inspect-templates.py --output my_results.json ``` +#### 검사 플래그 + +| 플래그 | 효과 | 사용하는 경우 | +|---|---|---| +| `--offline` | 모든 네트워크 조회(의존성 최신성 보고)를 건너뜀 | 네트워크가 없거나, 빠르고 외부 환경에 영향받지 않는 실행이 필요할 때 | +| `--no-smoke` | 생성된 프로젝트를 uvicorn으로 띄우는 단계를 건너뜀 | 구조만 반복해서 수정할 때 (스모크 테스트가 가장 느린 단계임) | +| `--mypy` | 생성된 프로젝트를 mypy로 타입 검사 | 템플릿을 제출하기 전 (느리기 때문에 명시적으로 켜야 함) | +| `--templates` | 검사할 템플릿 목록 (쉼표로 구분) | 템플릿 하나를 작업할 때 | +| `--verbose` | 자세한 출력 | 실패 원인을 디버깅할 때 | +| `--output` | JSON 결과 파일 경로 | 여러 실행 결과를 나란히 보관할 때 | + +템플릿 하나를 대상으로 하는 빠른 로컬 반복 실행은 다음과 같습니다: + +```console +$ python scripts/inspect-templates.py \ + --templates fastapi-sqlmodel --offline --no-smoke --verbose +``` + +PR을 열기 전에는 전체 검사를 실행합니다: + +```console +$ python scripts/inspect-templates.py --templates fastapi-sqlmodel --mypy +``` + ### Makefile 명령 사용 ```console @@ -201,10 +285,17 @@ $ make inspect-template TEMPLATES="fastapi-default,fastapi-async-crud" - 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` 은 선택 사항입니다 +- `pyproject.toml-tpl`(PEP 621)을 포함해야 합니다. 번들 템플릿은 pyproject를 + 우선하며 더 이상 `setup.py-tpl`을 제공하지 않습니다. 서드파티 템플릿이 계속 + 동작하도록 검사 단계에서는 여전히 `setup.py-tpl`을 허용하지만, 새 템플릿에는 + 추가하면 안 됩니다. +- `pyproject.toml-tpl` 이 `[project].dependencies` 를 선언한다면 `requirements.txt-tpl` 은 선택 사항입니다. + 다만 두 파일이 모두 있다면 내용이 서로 일치해야 합니다 (*설정 일관성* 참고) +- Python 3.12를 일관되게 대상으로 해야 합니다: `requires-python = ">=3.12"`, black + `target-version = ["py312"]`, mypy `python_version = "3.12"`, 그리고 Docker 베이스 + 이미지는 `python:3.12-slim`을 사용 +- `FastAPI-fastkit`을 런타임 의존성으로 선언하면 안 됩니다 +- 생성 후 치환되지 않은 ``가 남아 있으면 안 됩니다 ### FastAPI 요구 사항 diff --git a/docs/ko/reference/translation-status.md b/docs/ko/reference/translation-status.md index 7f55fd0..cc65d64 100644 --- a/docs/ko/reference/translation-status.md +++ b/docs/ko/reference/translation-status.md @@ -19,14 +19,14 @@ FastAPI-fastkit 문서는 여러 언어로 빌드되지만, 모든 번역이 ** | 언어 | 상태 | 마크다운 페이지 | 비고 | |---|---|---:|---| | 🇬🇧 English (`en`) | ✅ 원본 | 29 / 29 | 기준이 되는 원문입니다. | -| 🇰🇷 한국어 (`ko`) | 🟡 일부 | 26 / 29 | Phase 1~3은 모두 반영되어 있습니다. v1.4.0에서 추가된 튜토리얼 3편(`tutorial/auth-jwt.md`, `tutorial/sqlmodel.md`, `tutorial/llm-agent.md`)은 아직 번역 전이라 영문으로 폴백됩니다. `docs/ko/changelog.md` 는 영문 기준 `CHANGELOG.md` 를 그대로 사용합니다. | -| 🇯🇵 일본어 (`ja`) | 🟡 일부 | 26 / 29 | Phase 1~3은 모두 반영되어 있습니다. v1.4.0에서 추가된 튜토리얼 3편(`tutorial/auth-jwt.md`, `tutorial/sqlmodel.md`, `tutorial/llm-agent.md`)은 아직 번역 전이라 영문으로 폴백됩니다. `docs/ja/changelog.md` 는 영문 기준 `CHANGELOG.md` 를 그대로 사용합니다. | -| 🇨🇳 중국어 (`zh`) | 🟡 일부 | 26 / 29 | Phase 1~3은 모두 반영되어 있습니다. v1.4.0에서 추가된 튜토리얼 3편(`tutorial/auth-jwt.md`, `tutorial/sqlmodel.md`, `tutorial/llm-agent.md`)은 아직 번역 전이라 영문으로 폴백됩니다. `docs/zh/changelog.md` 는 영문 기준 `CHANGELOG.md` 를 그대로 사용합니다. | -| 🇪🇸 스페인어 (`es`) | 🟡 일부 | 26 / 29 | Phase 1~3은 모두 반영되어 있습니다. v1.4.0에서 추가된 튜토리얼 3편(`tutorial/auth-jwt.md`, `tutorial/sqlmodel.md`, `tutorial/llm-agent.md`)은 아직 번역 전이라 영문으로 폴백됩니다. `docs/es/changelog.md` 는 영문 기준 `CHANGELOG.md` 를 그대로 사용합니다. | -| 🇫🇷 프랑스어 (`fr`) | 🟡 일부 | 26 / 29 | Phase 1~3은 모두 반영되어 있습니다. v1.4.0에서 추가된 튜토리얼 3편(`tutorial/auth-jwt.md`, `tutorial/sqlmodel.md`, `tutorial/llm-agent.md`)은 아직 번역 전이라 영문으로 폴백됩니다. `docs/fr/changelog.md` 는 영문 기준 `CHANGELOG.md` 를 그대로 사용합니다. | -| 🇩🇪 독일어 (`de`) | 🟡 일부 | 26 / 29 | Phase 1~3은 모두 반영되어 있습니다. v1.4.0에서 추가된 튜토리얼 3편(`tutorial/auth-jwt.md`, `tutorial/sqlmodel.md`, `tutorial/llm-agent.md`)은 아직 번역 전이라 영문으로 폴백됩니다. `docs/de/changelog.md` 는 영문 기준 `CHANGELOG.md` 를 그대로 사용합니다. | +| 🇰🇷 한국어 (`ko`) | ✅ 완료 | 29 / 29 | Phase 1~3은 모두 반영되어 있습니다. 2026-10-11에 v1.4.1 영문 원본과 다시 동기화했습니다. v1.4.0에서 추가된 튜토리얼 3편(`tutorial/auth-jwt.md`, `tutorial/sqlmodel.md`, `tutorial/llm-agent.md`)을 번역했고, 처음 번역한 뒤 영문과 어긋난 페이지도 다시 맞췄습니다. `docs/ko/changelog.md` 는 영문 기준 `CHANGELOG.md` 를 그대로 사용합니다. | +| 🇯🇵 일본어 (`ja`) | ✅ 완료 | 29 / 29 | Phase 1~3은 모두 반영되어 있습니다. 2026-10-11에 v1.4.1 영문 원본과 다시 동기화했습니다. v1.4.0에서 추가된 튜토리얼 3편(`tutorial/auth-jwt.md`, `tutorial/sqlmodel.md`, `tutorial/llm-agent.md`)을 번역했고, 처음 번역한 뒤 영문과 어긋난 페이지도 다시 맞췄습니다. `docs/ja/changelog.md` 는 영문 기준 `CHANGELOG.md` 를 그대로 사용합니다. | +| 🇨🇳 중국어 (`zh`) | ✅ 완료 | 29 / 29 | Phase 1~3은 모두 반영되어 있습니다. 2026-10-11에 v1.4.1 영문 원본과 다시 동기화했습니다. v1.4.0에서 추가된 튜토리얼 3편(`tutorial/auth-jwt.md`, `tutorial/sqlmodel.md`, `tutorial/llm-agent.md`)을 번역했고, 처음 번역한 뒤 영문과 어긋난 페이지도 다시 맞췄습니다. `docs/zh/changelog.md` 는 영문 기준 `CHANGELOG.md` 를 그대로 사용합니다. | +| 🇪🇸 스페인어 (`es`) | ✅ 완료 | 29 / 29 | Phase 1~3은 모두 반영되어 있습니다. 2026-10-11에 v1.4.1 영문 원본과 다시 동기화했습니다. v1.4.0에서 추가된 튜토리얼 3편(`tutorial/auth-jwt.md`, `tutorial/sqlmodel.md`, `tutorial/llm-agent.md`)을 번역했고, 처음 번역한 뒤 영문과 어긋난 페이지도 다시 맞췄습니다. `docs/es/changelog.md` 는 영문 기준 `CHANGELOG.md` 를 그대로 사용합니다. | +| 🇫🇷 프랑스어 (`fr`) | ✅ 완료 | 29 / 29 | Phase 1~3은 모두 반영되어 있습니다. 2026-10-11에 v1.4.1 영문 원본과 다시 동기화했습니다. v1.4.0에서 추가된 튜토리얼 3편(`tutorial/auth-jwt.md`, `tutorial/sqlmodel.md`, `tutorial/llm-agent.md`)을 번역했고, 처음 번역한 뒤 영문과 어긋난 페이지도 다시 맞췄습니다. `docs/fr/changelog.md` 는 영문 기준 `CHANGELOG.md` 를 그대로 사용합니다. | +| 🇩🇪 독일어 (`de`) | ✅ 완료 | 29 / 29 | Phase 1~3은 모두 반영되어 있습니다. 2026-10-11에 v1.4.1 영문 원본과 다시 동기화했습니다. v1.4.0에서 추가된 튜토리얼 3편(`tutorial/auth-jwt.md`, `tutorial/sqlmodel.md`, `tutorial/llm-agent.md`)을 번역했고, 처음 번역한 뒤 영문과 어긋난 페이지도 다시 맞췄습니다. `docs/de/changelog.md` 는 영문 기준 `CHANGELOG.md` 를 그대로 사용합니다. | -*스냅샷 검증 시점: 2026-09-04, v1.4.0 릴리스 기준. 영문에 튜토리얼 3편(`tutorial/auth-jwt.md`, `tutorial/sqlmodel.md`, `tutorial/llm-agent.md`)이 추가되면서 나머지 로케일은 모두 ✅ 완료에서 🟡 일부(26 / 29)로 내려왔습니다. 같은 릴리스에서 한국어 랜딩 페이지·CLI 레퍼런스·스타터 선택 가이드는 영문 원문과 다시 동기화했고, 나머지 한국어 페이지는 존재하지만 v1.4.0 영문 갱신 이전 내용입니다.* 이 표는 수동으로 관리됩니다. 리포지토리 루트에서 현재 상태를 다시 세고 싶다면 다음 명령을 실행하세요: +*스냅샷 검증 시점: 2026-10-11, v1.4.1 릴리스 이후. 번역된 6개 로케일을 모두 영문 원본과 같은 수준으로 맞췄습니다. v1.4.0에서 추가된 튜토리얼 3편을 번역했고, 처음 번역한 뒤 영문과 어긋난 페이지도 모두 다시 동기화했습니다. 그래도 한 릴리스 안에서는 번역이 뒤처질 수 있으므로, 번역된 페이지가 영문과 다르다면 영문을 신뢰하세요.* 이 표는 수동으로 관리됩니다. 리포지토리 루트에서 현재 상태를 다시 세고 싶다면 다음 명령을 실행하세요: ```console $ for loc in en ko ja zh es fr de; do @@ -39,6 +39,7 @@ $ for loc in en ko ja zh es fr de; do 범례: - ✅ **원본 (Source of truth)** — 문서를 작성할 때 기준으로 삼는 언어입니다. +- ✅ **완료 (Complete)** — 모든 영어 페이지에 대응하는 번역 페이지가 있습니다. 다만 한 릴리스 안에서는 개별 페이지가 영어 갱신 내용보다 뒤처져 있을 수 있습니다. - 🟡 **부분 번역 (Partial)** — 일부 페이지만 번역된 상태입니다. 번역되지 않은 페이지는 영어 원문이 대신 표시됩니다. - 🔴 **기본 구조만 있음 (Skeleton)** — 언어 선택기에는 보이지만, 번역된 페이지가 아직 체크인되지 않은 상태입니다. 모든 페이지가 번역된 탐색 라벨 아래에서 영어 원문으로 표시됩니다. diff --git a/docs/ko/tutorial/auth-jwt.md b/docs/ko/tutorial/auth-jwt.md new file mode 100644 index 0000000..0a1efac --- /dev/null +++ b/docs/ko/tutorial/auth-jwt.md @@ -0,0 +1,315 @@ +# `fastapi-auth-jwt`로 JWT 인증 구현 + +첫 커밋부터 실제 계정 기능을 갖춘 API를 만들어 봅니다. 이 튜토리얼에서는 +`fastapi-auth-jwt` 템플릿을 처음부터 끝까지 따라가면서 토큰 쌍을 발급하고 +회전하는 방식, 재사용된 refresh 토큰이 거부되는 이유, 역할(role)과 +스코프(scope) 가드가 연결되는 방식, 그리고 SQLite에서 다른 데이터베이스로 +옮길 때 무엇이 달라지는지 살펴봅니다. + +## 배우는 내용 + +- `fastkit startdemo fastapi-auth-jwt`로 프로젝트를 생성하는 방법 +- access 토큰과 refresh 토큰의 차이, 그리고 토큰 회전(rotation)이 중요한 이유 +- `get_current_user`, `get_current_active_superuser`, `require_scopes(...)`가 + 라우트를 보호하는 방식 +- 비밀번호 해싱이 이루어지는 위치와 argon2id를 사용하는 이유 +- 번들된 SQLite 파일에서 Alembic을 사용해 PostgreSQL로 옮기는 방법 + +## 사전 요구 사항 + +- Python 3.12+ +- FastAPI-fastkit 설치 (`pip install fastapi-fastkit`) +- FastAPI 의존성과 pydantic 스키마를 다루는 데 익숙함 + +FastAPI 앱을 한 번도 만들어 본 적이 없다면 +[기본 API 서버 구축](basic-api-server.md)을 먼저 마치고 돌아오세요. 이 +템플릿은 기본 개념을 이미 알고 있다고 가정합니다. + +## 1단계: 프로젝트 생성 + +```console +$ fastkit startdemo fastapi-auth-jwt +Enter the project name: accounts-api +Enter the author name: Developer Kim +Enter the author email: developer@example.com +Enter the project description: Accounts and sessions service +Select package manager (pip, uv, pdm, poetry) [uv]: uv +Do you want to proceed with project creation? [y/N]: y +``` + +그다음 서버를 실행합니다: + +```console +$ cd accounts-api +$ bash scripts/run-server.sh # 또는: uvicorn src.app.main:app --reload +``` + +앱은 SQLite(`sqlite:///./app.db`)로 시작하고 시작 시점에 테이블을 직접 +생성하므로, 먼저 설치하거나 설정해야 할 것이 없습니다. API 문서는 +에서 확인할 수 있습니다. + +!!! tip "생성하기 전에 미리 보기" + `fastkit startdemo fastapi-auth-jwt --dry-run`은 아무 파일도 쓰지 않고 + 파일 트리와 패키지 목록만 출력합니다. 어떤 결과물을 받게 될지 미리 + 확인할 때 유용합니다. + +## 2단계: 생성된 트리 + +``` +accounts-api/ +├── pyproject.toml # PEP 621 메타데이터 + [tool.fastapi-fastkit] +├── requirements.txt +├── alembic.ini +├── Dockerfile +├── .env # SECRET_KEY, DATABASE_URL, 토큰 수명 +├── scripts/ +│ ├── format.sh lint.sh run-server.sh test.sh +├── src/ +│ └── app/ +│ ├── main.py # FastAPI() + lifespan + CORS + api_router +│ ├── core/ +│ │ ├── config.py # pydantic-settings +│ │ └── security.py # argon2 해싱 + JWT 인코딩/디코딩 +│ ├── db/session.py # 엔진 + get_session 의존성 +│ ├── api/ +│ │ ├── router.py # health와 모든 도메인 라우터를 집계 +│ │ ├── health.py # GET /health +│ │ └── deps.py # 세션, 현재 사용자, 역할/스코프 가드 +│ ├── domains/ +│ │ ├── auth/ # 로그인, refresh 회전, 로그아웃 +│ │ └── users/ # 회원 가입과 프로필 +│ └── alembic/ # 마이그레이션 환경 + versions/ +└── tests/ + ├── conftest.py test_health.py test_auth.py test_users.py +``` + +레이아웃은 `domain-starter`와 같은 형태입니다. 각 도메인은 `router.py`(전송), +`service.py`(비즈니스 로직), `repository.py`(데이터 접근), `models.py`(테이블), +`schemas.py`(API 입출력)를 소유합니다. 인증과 관련된 코드는 `auth/`에, +계정과 관련된 코드는 `users/`에 있으며, 두 도메인은 서로의 내부 구현에 +접근하지 않습니다. + +## 3단계: 회원 가입, 로그인, 보호된 라우트 호출 + +```console +# 1. 회원 가입 +$ curl -X POST http://127.0.0.1:8000/api/v1/auth/register \ + -H 'Content-Type: application/json' \ + -d '{"email": "alice@example.com", "password": "s3cret-password"}' + +# 2. 로그인: 폼 인코딩을 사용한다는 점에 유의하세요. OAuth2 password flow입니다 +$ curl -X POST http://127.0.0.1:8000/api/v1/auth/login \ + -d 'username=alice@example.com&password=s3cret-password' +{"access_token": "...", "refresh_token": "...", "token_type": "bearer"} + +# 3. 보호된 라우트 호출 +$ curl http://127.0.0.1:8000/api/v1/users/me \ + -H "Authorization: Bearer $ACCESS_TOKEN" +``` + +Swagger의 "Authorize" 버튼도 같은 흐름으로 동작하므로, 브라우저를 벗어나지 +않고 `/docs`에서 보호된 라우트를 직접 호출해 볼 수 있습니다. + +전체 엔드포인트는 다음과 같습니다: + +| 메서드 | 엔드포인트 | 인증 | 설명 | +|--------|-----------------------------|---------------|-----------------------------------| +| GET | `/api/v1/health` | — | Liveness probe | +| POST | `/api/v1/auth/register` | — | 계정 생성 | +| POST | `/api/v1/auth/login` | — | OAuth2 password 폼 → 토큰 쌍 | +| POST | `/api/v1/auth/refresh` | refresh 토큰 | 새 토큰 쌍으로 회전 | +| POST | `/api/v1/auth/logout` | refresh 토큰 | refresh 토큰 하나를 폐기 | +| POST | `/api/v1/auth/logout-all` | access 토큰 | 사용자의 모든 세션을 폐기 | +| GET | `/api/v1/users/me` | access 토큰 | 현재 사용자의 프로필 | +| PATCH | `/api/v1/users/me` | access 토큰 | 현재 사용자의 프로필 수정 | +| GET | `/api/v1/users/me/scopes` | 스코프 `me` | 스코프로 보호되는 예제 라우트 | +| GET | `/api/v1/users` | superuser | 역할로 보호되는 사용자 목록 | + +## 4단계: 토큰이 두 개인 이유 + +로그인하면 토큰 **쌍**이 반환됩니다: + +- **access 토큰**은 수명이 짧고(`ACCESS_TOKEN_EXPIRE_MINUTES`, 기본값 30분) + 모든 요청에 함께 전송됩니다. 수명이 짧기 때문에 유출되더라도 금방 쓸 수 + 없게 됩니다. +- **refresh 토큰**은 수명이 길고(`REFRESH_TOKEN_EXPIRE_DAYS`, 기본값 14일) + 새 토큰 쌍을 받는 단 한 가지 용도로만 사용됩니다. + +여기까지는 특별할 것이 없습니다. 이 템플릿이 흔한 JWT 예제 코드와 다른 +점은 refresh 토큰을 **추적하고 회전한다**는 것입니다. 발급되는 모든 refresh +토큰에는 `jti` 클레임이 붙고, 그 `jti`가 데이터베이스에 기록됩니다: + +```python +def issue_token_pair(self, user: User) -> TokenPair: + scopes = user.scope_list + access_token, _, _ = create_access_token(str(user.id), scopes) + refresh_token, jti, expires_at = create_refresh_token(str(user.id), scopes) + self.refresh_tokens.add(jti, user.id or 0, expires_at) + return TokenPair(access_token=access_token, refresh_token=refresh_token) +``` + +토큰을 갱신할 때는 새 쌍을 발급하기 **전에** 제출된 토큰을 먼저 폐기합니다: + +```python +def rotate(self, refresh_token: str) -> TokenPair: + payload = decode_token(refresh_token, REFRESH_TOKEN_TYPE) + record = self.refresh_tokens.get_by_jti(str(payload.get("jti"))) + if record is None or record.revoked: + raise TokenError("refresh token is no longer valid") + ... + self.refresh_tokens.revoke(record) + return self.issue_token_pair(user) +``` + +직접 확인해 보세요. 한 번 갱신한 다음, *이전* refresh 토큰을 다시 보내 봅니다. + +```console +$ curl -X POST http://127.0.0.1:8000/api/v1/auth/refresh \ + -H 'Content-Type: application/json' \ + -d "{\"refresh_token\": \"$REFRESH_TOKEN\"}" +# → 새 토큰 쌍 + +$ curl -X POST http://127.0.0.1:8000/api/v1/auth/refresh \ + -H 'Content-Type: application/json' \ + -d "{\"refresh_token\": \"$REFRESH_TOKEN\"}" +# → 401: 토큰은 사용된 순간 무효화되었습니다 +``` + +바로 이 한 가지 성질 덕분에, 탈취된 refresh 토큰이 영구적인 백도어가 되지 +않고 한 번만 쓸 수 있으며 탐지 가능한 실패로 끝납니다. + +로그아웃도 같은 방식으로 동작합니다. 이것 역시 일반적인 JWT 구성에서는 +대개 할 수 없는 기능입니다. `POST /auth/logout`은 토큰 하나를 폐기하고, +`POST /auth/logout-all`은 해당 사용자에게 속한 모든 refresh 토큰을 +폐기합니다. + +## 5단계: 비밀번호 + +해싱은 `src/app/core/security.py`에서 이루어지며, `pwdlib`을 통해 +**argon2id**를 사용합니다. argon2id는 레거시 bcrypt 구성이 아니라 현재 +권장되는 기본값입니다. 라우터와 서비스는 비밀번호를 검증하는 순간을 제외하면 +평문 비밀번호를 볼 일이 없고, `hashed_password`는 어떤 응답 스키마에도 +포함되지 않습니다. + +여기에서는 따로 설정할 것이 없습니다. 여러분이 *반드시* 설정해야 하는 것은 +서명 키 하나뿐입니다. + +## 6단계: 역할과 스코프 + +FastAPI 의존성 형태로 세 가지 가드가 제공됩니다: + +```python +CurrentUser = Annotated[User, Depends(get_current_user)] +``` + +- **`get_current_user`**는 bearer access 토큰을 디코딩해 사용자를 불러오고, + 비활성 계정은 `403`으로 거부합니다. 형식이 잘못되었거나, 만료되었거나, + 삭제된 사용자를 가리키는 토큰은 모두 `401`을 반환합니다. +- **`get_current_active_superuser`**는 그 위에 `is_superuser` 검사를 한 단계 + 더 얹습니다. `GET /api/v1/users`가 이 가드를 사용합니다. +- **`require_scopes(...)`**는 특정 스코프를 요구하는 의존성을 만들어 줍니다: + +```python +from fastapi import Depends +from src.app.api.deps import require_scopes + +@router.get("/reports", dependencies=[Depends(require_scopes("reports"))]) +def read_reports() -> list[str]: + ... +``` + +스코프는 사용자 행(row)에 쉼표로 구분된 문자열(`scopes: str`)로 저장되고, +`scope_list` 프로퍼티를 통해 다시 읽히며, 발급되는 모든 토큰에 복사됩니다. +스타터 템플릿에서 동작하는 가장 단순한 방식을 의도적으로 택한 것입니다. +권한 모델이 문자열 하나로 감당할 수 없을 만큼 커지면, 이 컬럼을 역할 +테이블로 바꾸고 의존성 시그니처는 그대로 유지하세요. 그러면 라우터 쪽은 +아무것도 옮길 필요가 없습니다. + +## 7단계: 보호된 도메인 추가 + +예를 들어 로그인한 사용자만 접근할 수 있는 `orders` 도메인을 추가하려면 +다음과 같이 진행합니다: + +1. `users/`와 같은 구조로 `src/app/domains/orders/`를 만듭니다 + (`models.py`, `schemas.py`, `repository.py`, `service.py`, `router.py`). +2. 계정이 필요한 라우트에서는 `CurrentUser`를 매개변수로 받거나, 라우터에 + `Depends(require_scopes("orders"))`를 붙입니다. +3. `src/app/api/router.py`의 `# fastkit:routes` 앵커 위에 라우터를 등록합니다. + `fastkit addroute`도 같은 앵커에 코드를 쓰기 때문에, 직접 작성한 라우트와 + 생성된 라우트가 한곳에 모입니다. +4. 마이그레이션을 생성합니다: `alembic revision --autogenerate -m "add orders"`. +5. 새 엔드포인트용으로 `tests/test_users.py`와 같은 형태의 테스트를 작성합니다. + +## 8단계: 지금은 SQLite, 나중에는 PostgreSQL + +기본 `DATABASE_URL`은 로컬 SQLite 파일이며, 앱은 시작할 때 +`create_db_and_tables()`를 호출합니다. 그래서 `pytest`와 `uvicorn` 모두 별도 +설정 없이 동작합니다. 로컬 개발을 넘어서는 환경에서는 Alembic이 스키마를 +관리하도록 맡기세요: + +```console +$ export DATABASE_URL='postgresql+psycopg://postgres:postgres@localhost:5432/app_db' +$ alembic upgrade head +``` + +모델을 변경한 뒤에는 다음 명령을 실행합니다: + +```console +$ alembic revision --autogenerate -m "add profile fields" +``` + +## 9단계: 배포 전에 + +`.env`에 있는 두 설정이 이 앱을 데모로 둘지 실제 서비스로 쓸지를 결정합니다: + +- **`SECRET_KEY`**는 모든 토큰에 서명합니다. 값을 설정하지 않으면 프로세스마다 + 임의의 키가 생성됩니다. 그러면 재시작할 때마다 모든 사용자가 로그아웃되고, + 워커가 여러 개일 때 워커마다 유효한 토큰을 다르게 판단하게 됩니다. 키를 + 하나 생성해 고정해 두세요: + + ```console + $ python -c "import secrets; print(secrets.token_urlsafe(32))" + ``` + +- **`DATABASE_URL`**은 사용할 데이터베이스를 선택합니다. 기본값인 SQLite는 + 프로덕션용 저장소가 아닙니다. + +필요하다면 `FIRST_SUPERUSER_EMAIL`과 `FIRST_SUPERUSER_PASSWORD`를 설정해 +시작 시점에 superuser 계정을 미리 만들어 둘 수 있고, `BACKEND_CORS_ORIGINS`에 +프런트엔드 origin 목록을 쉼표로 구분해 지정할 수 있습니다. + +## 10단계: 테스트 + +```console +$ bash scripts/test.sh # 또는: pytest +``` + +테스트 스위트는 인메모리 SQLite 데이터베이스를 대상으로 `httpx.AsyncClient`를 +실행합니다. 회원 가입 → 로그인 → me → 갱신 → 로그아웃으로 이어지는 정상 +경로 전체와, 실제로 중요한 거부 경로(잘못된 비밀번호, 형식이 잘못된 토큰, +만료된 토큰, 4단계에서 다룬 재사용된 refresh 토큰)를 검증합니다. 인증 규칙을 +확장할 때는 이 거부 테스트부터 먼저 확장하세요. 망가진 가드를 잡아내는 +테스트가 바로 이 테스트들입니다. + +## 정리 + +- **생성**: `fastkit startdemo fastapi-auth-jwt` → + `bash scripts/run-server.sh` → `/docs`에서 문서 확인. +- **토큰**: 요청에는 수명이 짧은 access 토큰을 사용하고, refresh 토큰은 + `jti`로 추적하며 회전시키므로 재사용이 실패하고 로그아웃이 실제로 + 동작합니다. +- **가드**: `get_current_user`, `get_current_active_superuser`, + `require_scopes(...)`를 일반 의존성으로 사용합니다. +- **저장소**: SQLite는 설정 없이 바로 쓸 수 있고, PostgreSQL은 + `DATABASE_URL`을 설정하고 `alembic upgrade head`를 실행하면 됩니다. +- **배포 전**: `SECRET_KEY`를 고정하고, `DATABASE_URL`이 실제 데이터베이스를 + 가리키도록 설정합니다. + +## 다음 단계 + +- [SQLModel 영속성](sqlmodel.md): 같은 도메인 레이아웃을 바탕으로 비동기 데이터 + 접근, 마이그레이션, 제네릭 CRUD에 집중합니다. +- [도메인 지향 프로젝트](domain-starter.md): 이 템플릿의 기반이 되는 + 레이아웃을 인증 기능 없이 다룹니다. +- [어떤 스타터를 선택해야 할까?](../user-guide/choosing-a-starter.md): 이 + 템플릿을 다른 템플릿과 비교합니다. diff --git a/docs/ko/tutorial/llm-agent.md b/docs/ko/tutorial/llm-agent.md new file mode 100644 index 0000000..44b067b --- /dev/null +++ b/docs/ko/tutorial/llm-agent.md @@ -0,0 +1,261 @@ +# `fastapi-llm-agent`로 스트리밍 LLM 에이전트 구현 + +토큰이 생성되는 대로 스트리밍하고, 실제로 무언가를 *수행*할 수 있는 채팅 +백엔드를 만들어 봅니다. 이 백엔드는 도구를 호출하고, 그 결과를 읽고, 이어서 +작업을 계속할 수 있습니다. 이 튜토리얼에서는 `fastapi-llm-agent` 템플릿을 +따라가면서 SSE 이벤트 프로토콜, 도구 호출 루프와 반복 횟수 상한, 인터페이스 +뒤에 둔 대화 메모리, 그리고 네트워크에 전혀 접근하지 않는 테스트 스위트를 +살펴봅니다. + +## 배우는 내용 + +- `fastkit startdemo fastapi-llm-agent`로 프로젝트를 생성하는 방법 +- 클라이언트가 처리해야 하는 SSE 이벤트 타입과 각 이벤트의 의미 +- 도구 호출 루프의 한 턴이 단계별로 동작하는 방식 +- 직접 도구를 추가하는 방법 +- 프로세스 내부 대화 저장소를 실제 저장소로 교체하는 방법 +- 테스트가 결정적(deterministic)이고 비용 없이 실행되는 이유 + +## 사전 요구 사항 + +- Python 3.12+ +- FastAPI-fastkit 설치 (`pip install fastapi-fastkit`) +- Anthropic API 키: 실제 모델과 통신할 때만 필요합니다. 생성된 테스트 + 스위트는 키 없이 실행됩니다. +- `async`/`await`와 비동기 제너레이터에 익숙함 + +## 1단계: 생성과 설정 + +```console +$ fastkit startdemo fastapi-llm-agent +Enter the project name: support-agent +Enter the author name: Developer Kim +Enter the author email: developer@example.com +Enter the project description: Streaming support assistant +Select package manager (pip, uv, pdm, poetry) [uv]: uv +Do you want to proceed with project creation? [y/N]: y +``` + +처음 실행하기 전에 `.env`에 키를 넣어 두세요: + +```bash +ANTHROPIC_API_KEY=sk-ant-... +LLM_MODEL=claude-sonnet-5 +LLM_MAX_TOKENS=4096 +MAX_TOOL_ITERATIONS=5 +``` + +키는 pydantic-settings가 읽어서 클라이언트를 생성할 때 전달합니다. 소스 +코드에 기록되지 않으며, Docker 이미지에도 포함되지 않습니다. + +```console +$ cd support-agent +$ bash scripts/run-server.sh +$ bash scripts/chat.sh "What is 12 * 34?" +``` + +!!! tip "Anthropic API 대신 게이트웨이 사용하기" + `LLM_BASE_URL`(게이트웨이가 bearer 인증을 사용한다면 `LLM_AUTH_TOKEN`도)을 + 설정하면 LiteLLM, 사내 프록시, 로컬 서버를 가리키게 할 수 있습니다. + 통신 형식은 그대로 Anthropic Messages API를 사용합니다. 다른 제공자에 + 맞게 변환하는 일은 앱이 아니라 게이트웨이가 담당합니다. + +## 2단계: 생성된 트리 + +``` +support-agent/ +├── pyproject.toml # PEP 621 메타데이터 + [tool.fastapi-fastkit] +├── requirements.txt +├── Dockerfile +├── .env +├── scripts/ +│ ├── chat.sh # 실행 중인 서버에 메시지 하나 전송 +│ ├── format.sh lint.sh run-server.sh test.sh +├── src/ +│ └── app/ +│ ├── main.py # FastAPI 앱 + lifespan +│ ├── core/config.py # pydantic-settings (API 키를 여기서 읽음) +│ ├── llm/ +│ │ ├── client.py # AsyncAnthropic 팩토리 +│ │ ├── tools.py # 도구 스키마 + 핸들러 +│ │ └── agent.py # 스트리밍 에이전트 + 도구 루프 +│ ├── memory/ +│ │ ├── base.py # ConversationStore 인터페이스 +│ │ └── in_memory.py # 프로세스 로컬 구현 +│ ├── schemas/chat.py # 요청 본문 + SSE 이벤트 엔벨로프 +│ └── api/ +│ ├── router.py # health와 chat을 집계 +│ ├── deps.py # 저장소 / 에이전트 의존성 +│ ├── health.py # GET /health +│ └── chat.py # POST /chat (SSE), 대화 기록 +└── tests/ + ├── conftest.py # 시나리오대로 응답하는 가짜 Anthropic 클라이언트 + ├── test_health.py test_chat.py test_tools.py test_memory.py +``` + +먼저 눈여겨볼 경계가 세 가지 있습니다. **클라이언트**는 팩토리로 생성되므로 +테스트에서 다른 클라이언트로 바꿔 넣을 수 있습니다. **메모리**는 단순한 구현을 +갖춘 인터페이스이므로 프로덕션에서 다른 구현으로 바꿔 넣을 수 있습니다. +**도구**는 스키마 목록과 핸들러 딕셔너리로 구성되므로 도구를 추가할 때 파일 +하나만 수정하면 됩니다. + +## 3단계: 엔드포인트 + +| 메서드 | 엔드포인트 | 설명 | +|--------|------------------------------|-------------------------| +| GET | `/api/v1/health` | Liveness probe | +| POST | `/api/v1/chat` | 응답을 SSE로 스트리밍 | +| GET | `/api/v1/conversations/{id}` | 저장된 대화 기록 조회 | +| DELETE | `/api/v1/conversations/{id}` | 대화 삭제 | + +`POST /api/v1/chat`은 `{"message": "...", "conversation_id": "..."}`를 받습니다. +새 대화를 시작하려면 `conversation_id`를 생략하고, 마지막 `done` 이벤트에서 +부여된 id를 읽어 오세요. + +## 4단계: 스트림 프로토콜 + +각 SSE 메시지는 `event:` 필드에 타입을, `data:`에 JSON 엔벨로프를 담습니다. +클라이언트는 다섯 가지 이벤트와 오류 이벤트를 처리해야 합니다: + +| 이벤트 | 필드 | 의미 | +|---------------|---------------------------------|--------------------------------| +| `token` | `text` | 어시스턴트 응답의 일부 조각 | +| `tool_use` | `tool_name`, `tool_input` | Claude가 도구 호출을 요청함 | +| `tool_result` | `tool_name`, `tool_output` | 도구가 실행됨 | +| `usage` | `input_tokens`, `output_tokens` | 턴마다 사용한 토큰 양 | +| `done` | `conversation_id` | 턴이 끝남 | +| `error` | `detail` | 스트리밍 도중 오류가 발생함 | + +`tool_use`와 `tool_result`는 장식용 이벤트가 아닙니다. 이 이벤트가 있어야 +UI가 도구가 실행되는 동안 설명 없이 멈춰 있지 않고 "검색 중…" 같은 상태를 +표시할 수 있습니다. + +토큰 사용량은 턴마다 `app.agent` 로거에도 기록되므로, 클라이언트를 따로 +계측하지 않아도 서버 로그에서 사용량을 확인할 수 있습니다. + +## 5단계: 루프의 한 턴 + +`src/app/llm/agent.py`에 있는 `ChatAgent.stream`이 엔진의 전부입니다. 한 +턴은 다음과 같이 진행됩니다: + +1. 저장소에서 대화 기록을 불러옵니다. +2. 대화 기록, 새 사용자 메시지, 도구 스키마를 담아 스트리밍 요청을 엽니다. +3. 텍스트 조각이 도착할 때마다 `token` 이벤트로 내보냅니다. +4. 스트림이 끝나면 최종 메시지를 읽고, `usage` 이벤트를 내보낸 뒤 토큰 수를 + 로그로 기록합니다. +5. **`stop_reason != "tool_use"`라면** 어시스턴트의 응답이 끝난 것입니다. 턴을 + 저장하고 `done`을 내보낸 뒤 반환합니다. +6. **그렇지 않다면** 각 `tool_use` 블록마다 `tool_use`를 내보내고, 핸들러를 + 실행하고, `tool_result`를 내보낸 뒤 출력을 모읍니다. +7. 모든 도구 결과를 하나의 `user` 메시지로 묶어 다시 보냅니다. Messages API가 + 이 형식을 요구하기 때문입니다. 그다음 2단계부터 다시 반복합니다. + +루프는 `MAX_TOOL_ITERATIONS`(기본값 5)로 제한됩니다. Claude가 상한을 넘어서도 +계속 도구를 요청하면, 서버가 끝없이 루프를 도는 대신 턴을 저장하고 경고를 +로그로 남긴 뒤 `error` 이벤트를 내보냅니다: + +```python +yield AgentEvent( + type="error", + detail=( + "The assistant exceeded the tool-call limit of " + f"{self._max_tool_iterations} iterations." + ), +) +``` + +에이전트 루프에서 이런 상한은 선택 사항이 아닙니다. 상한이 없으면 도구 +결과를 잘못 이해한 모델이 끝없이 비용을 발생시킬 수 있습니다. + +## 6단계: 번들된 도구 + +| 도구 | 용도 | +|----------------|----------------------------------------------------------| +| `calculator` | 산술 계산. `eval`이 아니라 AST로 파싱한 뒤 허용 목록으로 검사 | +| `current_time` | IANA 시간대 기준의 현재 시각 | + +`calculator`는 한 번 더 살펴볼 만합니다. 이 도구는 `ast`로 수식을 파싱하고 +허용 목록에 있는 노드 타입만 순회하므로, 모델이 넘긴 도구 인자로 임의의 +Python 코드를 실행할 수 없습니다. 모델이 생성한 입력을 받는 도구라면 모두 +같은 원칙을 지켜야 합니다. **모델의 출력은 신뢰할 수 없는 입력입니다.** + +### 도구 추가 + +필요한 코드는 모두 `src/app/llm/tools.py`에 있습니다: + +1. `TOOL_SCHEMAS`에 스키마를 추가합니다. 스키마에는 `name`, `description`, + JSON Schema 형식의 `input_schema`가 들어갑니다. +2. 같은 이름으로 `TOOL_HANDLERS`에 핸들러를 등록합니다. + +제대로 챙겨야 할 점이 두 가지 있습니다: + +- **description은 Claude가 언제 이 도구를 사용해야 하는지 판단할 때 받는 + 유일한 지시입니다.** 즉 문서가 아니라 프롬프트 엔지니어링에 해당합니다. + 설명이 모호하면 도구가 전혀 호출되지 않거나, 반대로 끊임없이 호출됩니다. +- **핸들러는 문자열을 반환해야 하고, 자신의 실패는 예외를 던지는 대신 읽을 수 + 있는 메시지로 바꿔야 합니다.** 루프는 이 텍스트를 그대로 모델에 다시 + 전달합니다. 그래서 "X라는 도시를 찾을 수 없습니다" 같은 메시지를 받으면 + 모델이 상황을 복구할 수 있지만, 예외가 발생하면 턴이 끝나 버립니다. + +## 7단계: 대화 메모리 + +`ConversationStore`(`src/app/memory/base.py`)는 인터페이스이고, 번들된 +`in_memory.py`는 딕셔너리로 이를 구현합니다. 단일 프로세스에서는 충분하지만 +그 밖의 환경에서는 적합하지 않습니다. 재시작하면 모든 내용이 사라지고, +워커가 둘 이상이면 워커마다 대화 기록이 서로 달라집니다. + +실제 환경에서 쓰려면 같은 인터페이스를 Redis나 데이터베이스 기반으로 구현하고, +`src/app/api/deps.py`의 `get_store()`에서 그 구현을 반환하도록 바꾸세요. +에이전트와 라우터 모두 구체 클래스를 직접 참조하지 않으므로, 두 곳 모두 +수정할 필요가 없습니다. + +## 8단계: 테스트 + +```console +$ bash scripts/test.sh # 또는: pytest +``` + +`tests/conftest.py`는 `AsyncAnthropic.messages.stream`을 흉내 내는 가짜 +클라이언트로 미리 작성한 시나리오의 턴을 재생합니다. 그래서 테스트 스위트는 +결정적으로 동작하고, 오프라인에서 실행되며, 비용이 들지 않습니다. 그러면서도 +중요한 경로는 모두 검증합니다. 스트리밍 경로와 전체 도구 루프를 다루며, +도구를 호출한 뒤 모델이 답변하는 턴도 포함합니다. + +에이전트를 확장할 때도 이 패턴을 유지하세요. 실제 모델을 대상으로 LLM 통합을 +테스트하면 테스트 스위트가 느리고, 불안정하고, 비싸지며, 그런데도 모델이 무슨 +말을 할지는 검증할 수 없습니다. 시나리오를 따르는 클라이언트로 *루프*를 +테스트하면 여러분이 작성한 코드를 정확히 검증할 수 있습니다. + +## 9단계: 확장 지점 + +- **영구적인 대화 기록**: Redis나 데이터베이스 기반으로 `ConversationStore`를 + 구현합니다(7단계). +- **RAG**: 템플릿은 의도적으로 검색(retrieval) 기능을 범위에서 제외했습니다. + 자연스러운 확장 지점은 `ChatAgent.stream`입니다. 첫 요청 전에 컨텍스트를 + 가져와 시스템 프롬프트 앞에 붙이거나, 인덱스를 도구 하나로 노출해 모델이 + 검색할 시점을 결정하게 할 수 있습니다. +- **인증과 레이트 제한**: 기본 제공되는 `POST /chat`에는 인증이 없습니다. 공개적으로 + 노출하기 전에 라우터에 의존성을 추가하세요. 재사용할 수 있는 가드는 + [JWT 인증 튜토리얼](auth-jwt.md)에 있습니다. + +## 정리 + +- **생성**: `fastkit startdemo fastapi-llm-agent`, `ANTHROPIC_API_KEY` 설정, + `bash scripts/run-server.sh`. +- **스트리밍**: `token` / `tool_use` / `tool_result` / `usage` / `done` / + `error` 이벤트를 사용하는 SSE. +- **루프**: 스트리밍 → 도구 호출 → 결과를 하나의 user 메시지로 반환 → 반복. + 반복 횟수는 `MAX_TOOL_ITERATIONS`로 제한됩니다. +- **도구**: `TOOL_SCHEMAS`에 스키마를, `TOOL_HANDLERS`에 핸들러를 추가합니다. + 도구 입력은 신뢰할 수 없는 값으로 취급합니다. +- **메모리**: `ConversationStore` 인터페이스와 딕셔너리 구현을 제공하며, + `get_store()`에서 교체합니다. +- **테스트**: 시나리오를 따르는 가짜 클라이언트를 사용하므로 테스트 스위트가 + 오프라인에서 결정적으로 동작합니다. + +## 다음 단계 + +- [MCP 통합](mcp-integration.md): 채팅 에이전트가 아니라 MCP 서버가 필요할 때 + 참고하세요. +- [JWT 인증](auth-jwt.md): 채팅 엔드포인트 앞에 계정 기능을 추가합니다. +- [어떤 스타터를 선택해야 할까?](../user-guide/choosing-a-starter.md) diff --git a/docs/ko/tutorial/mcp-integration.md b/docs/ko/tutorial/mcp-integration.md index 6d6d743..735c1e9 100644 --- a/docs/ko/tutorial/mcp-integration.md +++ b/docs/ko/tutorial/mcp-integration.md @@ -68,8 +68,8 @@ Deploying FastAPI project using 'fastapi-mcp' template │ Dependency 1 │ fastapi │ │ Dependency 2 │ uvicorn │ │ Dependency 3 │ pydantic │ -│ Dependency 4 │ python-jose │ -│ Dependency 5 │ passlib │ +│ Dependency 4 │ PyJWT │ +│ Dependency 5 │ pwdlib[argon2] │ │ Dependency 6 │ python-multipart│ │ Dependency 7 │ mcp │ └──────────────┴────────────────┘ @@ -131,21 +131,21 @@ ai-integrated-api/ ```python from datetime import datetime, timedelta from typing import Optional, Dict, Any -from jose import JWTError, jwt -from passlib.context import CryptContext +import jwt +from pwdlib import PasswordHash from src.core.config import settings -# 비밀번호 해싱 -pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") +# 비밀번호 해싱: 새 애플리케이션에 권장되는 기본값인 argon2id를 사용합니다. +password_hash = PasswordHash.recommended() def verify_password(plain_password: str, hashed_password: str) -> bool: """Password verification""" - return pwd_context.verify(plain_password, hashed_password) + return password_hash.verify(plain_password, hashed_password) def get_password_hash(password: str) -> str: """Password hashing""" - return pwd_context.hash(password) + return password_hash.hash(password) def create_access_token(data: Dict[str, Any], expires_delta: Optional[timedelta] = None) -> str: """Access token generation""" @@ -189,7 +189,7 @@ def decode_token(token: str) -> Optional[Dict[str, Any]]: algorithms=[settings.ALGORITHM] ) return payload - except JWTError: + except jwt.PyJWTError: return None def verify_token(token: str, token_type: str = "access") -> Optional[str]: @@ -452,8 +452,7 @@ user_db = UserDatabase() from typing import Optional, List from fastapi import Depends, HTTPException, status, Security from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials, APIKeyHeader -from jose import JWTError - +import jwt from src.auth.jwt_handler import decode_token, token_manager from src.auth.models import User, UserInDB, Permission, user_db @@ -486,7 +485,7 @@ async def get_current_user( if user_id is None: raise credentials_exception - except JWTError: + except jwt.PyJWTError: raise credentials_exception user = user_db.get_user_by_id(user_id) diff --git a/docs/ko/tutorial/sqlmodel.md b/docs/ko/tutorial/sqlmodel.md new file mode 100644 index 0000000..956ac94 --- /dev/null +++ b/docs/ko/tutorial/sqlmodel.md @@ -0,0 +1,312 @@ +# `fastapi-sqlmodel`로 비동기 영속성 구현 + +각 개념을 **한 번만** 정의하는, 데이터베이스 기반 API를 만들어 봅니다. +SQLModel 클래스는 SQLAlchemy 테이블이면서 동시에 pydantic 스키마이기도 하므로, +컬럼 하나를 추가하려고 파일 네 개를 고칠 필요가 없습니다. 이 튜토리얼에서는 +`fastapi-sqlmodel` 템플릿을 따라가면서 모델 계층 구조, 제네릭 CRUD 베이스, +페이지네이션 응답, 비동기 Alembic 마이그레이션, 그리고 직접 도메인을 추가하는 +방법을 살펴봅니다. + +## 배우는 내용 + +- `fastkit startdemo fastapi-sqlmodel`로 프로젝트를 생성하는 방법 +- `ItemBase` → `Item` / `ItemCreate` / `ItemUpdate` / `ItemPublic` 구조가 + 스키마 중복을 없애는 방식 +- `CRUDBase`가 기본으로 제공하는 기능과 이를 확장하는 위치 +- `Page[T]` 목록 엔벨로프와 그 한도가 적용되는 위치 +- 비동기 Alembic 마이그레이션 실행과 SQLite에서 PostgreSQL로 전환하는 방법 + +## 사전 요구 사항 + +- Python 3.12+ +- FastAPI-fastkit 설치 (`pip install fastapi-fastkit`) +- `async`/`await`와 기본적인 SQL 개념에 익숙함 + +동기 방식의 Compose 기반 PostgreSQL 스택을 원한다면 +`fastapi-psql-orm`을 사용하는 [데이터베이스 통합](database-integration.md)을 +참고하세요. + +## 1단계: 생성과 실행 + +```console +$ fastkit startdemo fastapi-sqlmodel +Enter the project name: catalog-api +Enter the author name: Developer Kim +Enter the author email: developer@example.com +Enter the project description: Product catalog service +Select package manager (pip, uv, pdm, poetry) [uv]: uv +Do you want to proceed with project creation? [y/N]: y +``` + +```console +$ cd catalog-api +$ bash scripts/migrate.sh # alembic upgrade head +$ bash scripts/run-server.sh # 또는: uvicorn src.app.main:app --reload +``` + +기본 `DATABASE_URL`은 `sqlite+aiosqlite:///./app.db`이므로 데이터베이스 +서버를 따로 설치할 필요가 없습니다. API 문서는 +에서 확인할 수 있습니다. + +## 2단계: 생성된 트리 + +``` +catalog-api/ +├── pyproject.toml # PEP 621 메타데이터 + [tool.fastapi-fastkit] +├── requirements.txt +├── alembic.ini +├── Dockerfile +├── .env +├── scripts/ +│ ├── format.sh lint.sh migrate.sh run-server.sh test.sh +├── src/ +│ └── app/ +│ ├── main.py # FastAPI 앱 + lifespan +│ ├── core/config.py # pydantic-settings +│ ├── db/ +│ │ ├── session.py # 비동기 엔진, get_session 의존성 +│ │ └── base.py # Alembic용 메타데이터 집계 +│ ├── crud/ +│ │ ├── base.py # 제네릭 CRUDBase +│ │ └── pagination.py # Page[T] 엔벨로프 +│ ├── alembic/ +│ │ ├── env.py # 비동기 마이그레이션 환경 +│ │ └── versions/0001_create_items_table.py +│ ├── api/ +│ │ ├── router.py # health와 도메인 라우터를 집계 +│ │ └── health.py # GET /health +│ └── domains/items/ # 예제 도메인 +│ ├── models.py # Item 테이블 + Create/Update/Public 스키마 +│ ├── crud.py # ItemCRUD(CRUDBase) +│ └── router.py # FastAPI 라우터 +└── tests/ + ├── conftest.py test_health.py test_items.py test_crud_base.py +``` + +도메인 폴더에 *없는* 것에 주목하세요. `models.py`와 분리된 `schemas.py`가 +없고, 도메인마다 작성하는 repository 보일러플레이트도 없습니다. 앞의 것은 +SQLModel이, 뒤의 것은 `CRUDBase`가 대신합니다. + +## 3단계: 개념마다 모델 정의는 하나 + +`src/app/domains/items/models.py`를 열어 보세요. 모든 클래스가 하나의 베이스 +클래스에서 파생됩니다: + +```python +class ItemBase(SQLModel): + """Fields shared by the table and every request/response schema.""" + name: str = Field(min_length=1, max_length=128, index=True) + description: str | None = Field(default=None, max_length=512) + price: float = Field(ge=0) + in_stock: bool = True + + +class Item(ItemBase, table=True): # 실제 테이블 + __tablename__ = "items" + id: int | None = Field(default=None, primary_key=True) + created_at: datetime = Field(default_factory=_utcnow) + + +class ItemCreate(ItemBase): ... # POST 본문: 서버가 부여하는 필드 없음 + + +class ItemUpdate(SQLModel): # PATCH 본문: 모든 필드가 선택 사항 + name: str | None = Field(default=None, min_length=1, max_length=128) + ... + + +class ItemPublic(ItemBase): # 응답: 베이스 + 서버가 부여하는 필드 + id: int + created_at: datetime +``` + +클래스는 네 개이고 각자 맡은 일이 다르지만, 필드가 정의된 곳은 한 군데뿐입니다: + +| 클래스 | 역할 | 분리한 이유 | +|--------------|-------------------------------|-------------------| +| `ItemBase` | 공통 필드 정의 | 단일 진실 공급원(single source of truth) | +| `Item` | 데이터베이스 테이블 | `id`, `created_at` 추가 | +| `ItemCreate` | `POST` 요청 본문 | `id`를 받으면 안 됨 | +| `ItemUpdate` | `PATCH` 요청 본문 | 모든 필드가 선택 사항 | +| `ItemPublic` | 응답 본문 | 서버가 부여한 필드를 노출 | + +**얻는 이점:** `sku` 컬럼을 추가하려면 `ItemBase`에 한 줄만 추가하면 됩니다. +그러면 테이블에 컬럼이 생기고, 생성 요청 본문이 그 값을 받으며, 응답에도 +그 값이 포함됩니다. 다른 곳은 고칠 필요가 없습니다. 다만 `ItemUpdate`는 +의도적으로 명시적인 형태를 유지합니다. 부분 수정의 의미는 자동으로 파생되는 +것이 아니라 직접 결정해야 하는 사항이기 때문입니다. + +## 4단계: 다시 작성할 필요 없는 CRUD + +`src/app/crud/base.py`는 테이블 모델과 두 입력 스키마를 타입 매개변수로 받는 +제네릭 클래스입니다: + +```python +class CRUDBase(Generic[ModelType, CreateSchemaType, UpdateSchemaType]): + async def get(self, session, obj_id) -> ModelType | None: ... + async def list(self, session, *, offset=0, limit=20) -> Sequence[ModelType]: ... + async def count(self, session) -> int: ... + async def create(self, session, obj_in) -> ModelType: ... + async def update(self, session, db_obj, obj_in) -> ModelType: ... + async def delete(self, session, db_obj) -> None: ... +``` + +꼭 이해해 둘 세부 사항이 하나 있습니다. `update`는 `exclude_unset=True`를 +사용합니다: + +```python +changes: dict[str, Any] = obj_in.model_dump(exclude_unset=True) +``` + +바로 이 설정 덕분에 `PATCH`가 실제로 부분 수정으로 동작합니다. 클라이언트가 +보내지 않은 필드는 딕셔너리에 들어가지 않으므로 기록되지도 않습니다. +`exclude_unset`이 없다면 `PATCH`는 건드리지 않은 컬럼까지 스키마 기본값으로 +조용히 덮어쓰게 됩니다. + +도메인은 베이스를 상속하고, 그 도메인에만 필요한 기능만 추가합니다: + +```python +class ItemCRUD(CRUDBase[Item, ItemCreate, ItemUpdate]): + """CRUD for Item, extended with a name search.""" + + async def search_by_name(self, session, term, *, limit=20): + statement = select(Item).where(Item.name.like(f"%{term}%")).limit(limit) + result = await session.exec(statement) + return result.all() + + +item_crud = ItemCRUD(Item) +``` + +## 5단계: 페이지네이션된 목록 + +목록 엔드포인트는 배열을 그대로 반환하지 않고 `Page[T]` 엔벨로프로 감싸서 +반환합니다: + +```json +{"items": [...], "total": 42, "offset": 0, "limit": 20} +``` + +한도를 제한하는 작업은 클라이언트가 아니라 라우터에서 이루어집니다: + +```python +limit: int = Query(default=settings.DEFAULT_PAGE_LIMIT, ge=1) +... +limit = min(limit, settings.MAX_PAGE_LIMIT) +``` + +따라서 `?limit=100000`을 보내더라도 요청 하나가 전체 테이블 스캔으로 이어지지 +않습니다. `Page`는 `offset`, `len(items)`, `total`로부터 계산되는 `has_next` +프로퍼티도 제공합니다. 대개 프런트엔드가 실제로 필요로 하는 값이 바로 이 +값입니다. + +items 도메인이 제공하는 엔드포인트는 다음과 같습니다: + +| 메서드 | 엔드포인트 | 설명 | +|--------|---------------------------|--------------------------------| +| GET | `/api/v1/health` | Liveness probe | +| GET | `/api/v1/items` | item 목록 조회 (`offset`, `limit`) | +| GET | `/api/v1/items/{item_id}` | item 하나 조회 | +| POST | `/api/v1/items` | item 생성 (`201`) | +| PATCH | `/api/v1/items/{item_id}` | item 부분 수정 | +| DELETE | `/api/v1/items/{item_id}` | item 삭제 (`204`) | + +직접 호출해 보세요: + +```console +$ curl -X POST http://127.0.0.1:8000/api/v1/items \ + -H 'Content-Type: application/json' \ + -d '{"name": "Keyboard", "price": 89.0}' + +$ curl 'http://127.0.0.1:8000/api/v1/items?offset=0&limit=5' +``` + +## 6단계: 처음부터 끝까지 비동기 + +`src/app/db/session.py`는 프로세스마다 `AsyncEngine`을 하나 만들고, 요청 +범위의 세션을 제공합니다: + +```python +async def get_session() -> AsyncGenerator[AsyncSession, None]: + async with async_session_factory() as session: + yield session +``` + +라우터는 이 함수에 의존하고(`session: AsyncSession = Depends(get_session)`), +`CRUDBase`는 세션을 첫 번째 인자로 받으며, 모든 데이터베이스 호출은 +`await`로 실행됩니다. 요청 경로에서 I/O 때문에 이벤트 루프가 블로킹되는 +곳은 없습니다. + +`create_db_and_tables()`도 이 파일에 있으며 앱의 `lifespan`에서 호출됩니다. +이 함수는 기본 SQLite 구성과 테스트를 편하게 하려고 둔 것입니다. Alembic이 +스키마를 관리하기 시작하면 `CREATE_TABLES_ON_STARTUP=false`로 설정하세요. + +## 7단계: 도메인 추가 + +1. `items`와 같은 구조로 `src/app/domains//`을 만듭니다 + (`models.py`, `crud.py`, `router.py`). +2. **`src/app/db/base.py`에서 테이블 모델을 import합니다.** 잊기 쉽고, 잊었을 + 때 원인을 찾기 어려운 단계입니다. Alembic autogenerate와 + `create_db_and_tables`는 import된 모델만 인식하기 때문에, import를 빠뜨리면 + 오류가 나는 대신 빈 마이그레이션이 생성됩니다. +3. `src/app/api/router.py`에 라우터를 등록합니다. `# fastkit:imports`와 + `# fastkit:routes` 앵커는 `fastkit addroute`가 코드를 삽입하는 위치를 + 표시하므로, 직접 작성하는 코드도 같은 위치에 넣으세요. +4. 마이그레이션을 생성합니다: + `alembic revision --autogenerate -m "add "`. +5. `tests/test_items.py`와 같은 형태로 `tests/test_.py`를 추가합니다. + +## 8단계: 마이그레이션과 PostgreSQL + +`src/app/alembic/env.py`에 있는 Alembic 환경은 비동기로 동작하므로, 앱과 +같은 드라이버 스택을 사용해 실행됩니다. 평소에는 다음과 같이 사용합니다: + +```console +$ bash scripts/migrate.sh # upgrade head +$ alembic revision --autogenerate -m "add sku to items" # 모델을 변경한 뒤 +``` + +데이터베이스를 바꾸려면 URL을 변경하고 드라이버를 설치하면 됩니다: + +```console +$ uv pip install "asyncpg>=0.31.0" +$ export DATABASE_URL="postgresql+asyncpg://user:password@localhost:5432/app" +$ bash scripts/migrate.sh +``` + +모델, CRUD, 라우터는 전혀 바뀌지 않습니다. 드라이버를 `create_engine` 뒤에 +감춰 두는 이유가 바로 이것입니다. + +## 9단계: 테스트 + +```console +$ bash scripts/test.sh # 또는: pytest +``` + +각 테스트는 새로 만든 인메모리 SQLite 데이터베이스에서 실행됩니다. +마이그레이션도, 픽스처 파일도, 테스트 사이의 정리 작업도 필요하지 않습니다. +`test_crud_base.py`는 제네릭 베이스를 직접 검증합니다. 베이스를 확장하기 전에 +읽어 볼 만한 파일입니다. `exclude_unset` 동작과, 여러분의 도메인이 그대로 +물려받는 count/offset 계약이 이 파일에 기록되어 있기 때문입니다. + +## 정리 + +- **생성**: `fastkit startdemo fastapi-sqlmodel` → + `bash scripts/migrate.sh` → `bash scripts/run-server.sh`. +- **모델**: `ItemBase`가 필드를 보유하고 테이블과 입출력 스키마가 이를 + 상속하므로, 새 컬럼은 한 번만 선언합니다. +- **CRUD**: `CRUDBase`가 get / list / count / create / update / delete를 + 제공하고, 도메인은 이를 상속해 자신만의 쿼리를 추가합니다. +- **목록**: `Page[T]` 엔벨로프를 사용하며, 한도는 서버에서 제한합니다. +- **스키마**: 비동기 Alembic을 사용합니다. 새 모델은 `src/app/db/base.py`에서 + import해야 한다는 점을 기억하세요. +- **데이터베이스**: 기본값은 SQLite이고, `DATABASE_URL`을 변경하고 `asyncpg`를 + 설치하면 PostgreSQL을 사용할 수 있습니다. + +## 다음 단계 + +- [JWT 인증](auth-jwt.md): 이 데이터 계층 앞에 계정과 보호된 라우트를 + 추가합니다. +- [도메인 지향 프로젝트](domain-starter.md): 두 템플릿이 공유하는 레이아웃을 + 데이터베이스 없이 다룹니다. +- [어떤 스타터를 선택해야 할까?](../user-guide/choosing-a-starter.md) diff --git a/docs/ko/user-guide/cli-reference.md b/docs/ko/user-guide/cli-reference.md index 3a94276..9e67325 100644 --- a/docs/ko/user-guide/cli-reference.md +++ b/docs/ko/user-guide/cli-reference.md @@ -258,7 +258,7 @@ to proceed with project creation?" 확인 프롬프트에는 영향을 주지 | 축 | 선택지 | 설치되는 패키지 | 생성되는 파일 | |---|---|---|---| | `database` | PostgreSQL, MySQL, MongoDB, Redis, SQLite, None | PostgreSQL: `asyncpg`, `sqlalchemy` · MySQL: `aiomysql`, `sqlalchemy` · MongoDB: `motor` · Redis: `redis[hiredis]` · SQLite: `sqlalchemy`, `aiosqlite` | 프리셋 경로에 데이터베이스 설정 모듈 (예: `src/config/database.py`) | -| `authentication` | JWT, OAuth2, FastAPI-Users, Session-based, None | JWT: `python-jose[cryptography]`, `passlib[bcrypt]` · OAuth2: `authlib`, `itsdangerous`, `httpx` · FastAPI-Users: `fastapi-users[sqlalchemy]`, `python-jose[cryptography]`, `passlib[bcrypt]` · Session-based: `itsdangerous` | 프리셋 경로에 인증 설정 모듈 생성; OAuth2 · Session-based 는 `main.py` 미들웨어 설정도 추가 | +| `authentication` | JWT, OAuth2, FastAPI-Users, Session-based, None | JWT: `pyjwt[crypto]`, `pwdlib[argon2]` · OAuth2: `authlib`, `itsdangerous`, `httpx` · FastAPI-Users: `fastapi-users[sqlalchemy]`, `pyjwt[crypto]`, `pwdlib[argon2]` · Session-based: `itsdangerous` | 프리셋 경로에 인증 설정 모듈 생성; OAuth2 · Session-based 는 `main.py` 미들웨어 설정도 추가 | | `async_tasks` | Celery, Dramatiq, None | Celery: `celery[redis]`, `redis[hiredis]` · Dramatiq: `dramatiq[redis]`, `redis[hiredis]` | `/worker.py`(백그라운드 워커) + `/features/tasks.py`(작업 라우트) | | `testing` | Basic, Coverage, Advanced, None | Basic: `pytest`, `pytest-asyncio`, `httpx` · Coverage: + `pytest-cov` · Advanced: + `faker`, `factory-boy` | `pytest.ini`; Advanced 는 `tests/factories.py`, `tests/test_factories.py` 도 추가 | | `caching` | Redis, None | Redis: `redis[hiredis]`, `fastapi-cache2`, `jinja2` | `/features/cache.py`(캐시된 엔드포인트) | diff --git a/docs/ko/user-guide/creating-projects.md b/docs/ko/user-guide/creating-projects.md index 6ca13f7..62c8b54 100644 --- a/docs/ko/user-guide/creating-projects.md +++ b/docs/ko/user-guide/creating-projects.md @@ -462,7 +462,7 @@ settings = Settings()
```console -$ pip install requests httpx python-jose +$ pip install requests httpx pyjwt $ pip freeze > requirements.txt ``` diff --git a/docs/zh/contributing/development-setup.md b/docs/zh/contributing/development-setup.md index 96aed02..5896fa4 100644 --- a/docs/zh/contributing/development-setup.md +++ b/docs/zh/contributing/development-setup.md @@ -212,6 +212,27 @@ Success: no issues found in 12 source files | `make inspect-templates-verbose` | 以详细输出运行模板检查 | | `make inspect-template` | 检查指定模板(TEMPLATES 参数) | +迭代开发时,底层脚本有三个值得了解的选项:`--offline`(跳过 PyPI 版本新鲜度查询)、`--no-smoke`(跳过用 uvicorn 启动生成项目这一最慢的步骤)以及 `--mypy`(对生成项目进行类型检查;由于速度较慢,需要显式开启)。每项检查具体做什么,请参阅 [模板质量保障](../reference/template-quality-assurance.md)。 + +```console +$ python scripts/inspect-templates.py --templates fastapi-sqlmodel --offline --no-smoke +``` + +### 生成的代码存放在 Jinja2 片段中 + +`fastkit init --interactive` 写入项目的所有内容 —— `main.py`、数据库与认证模块、Docker 文件、pytest 配置 —— 都是由 `src/fastapi_fastkit/fragments/` 下的模板渲染而成,而不是在 Python 中拼接字符串得到的。`main.py.j2` 不硬编码任何功能:它遍历由 `DynamicConfigGenerator` 提供的 `import_fragments`、`lifespan_fragments` 和 `setup_fragments` 列表,并逐一包含,因此各项功能可以自由组合。 + +添加一个功能的大致步骤: + +1. 把各部分放入 `main/imports/`、`main/lifespan/` 和/或 `main/setup/`(lifespan 片段缩进四个空格;另外两种不缩进)。 +2. 在 `DynamicConfigGenerator._main_fragments()` 中注册它们,并以相应的配置查询作为条件。 +3. 对于独立模块,添加 `/.py.j2`,并用 `self._render(...)` 渲染。 +4. 扩展 `tests/test_backends/test_project_builder_config_generator.py` 中的组合矩阵 —— 它会断言每个渲染出的 Python 文件都能被 `ast.parse` 解析。 + +有两个容易踩坑的约束:每个片段都必须以换行符结尾(环境以 `keep_trailing_newline=True` 构建),并且 `main.py.j2` 必须保留 `# fastkit:imports` / `# fastkit:routes` 锚点 —— `fastkit addroute` 会在这些位置插入代码。 + +权威的操作流程与片段本身放在一起,位于 [`src/fastapi_fastkit/fragments/README.md`](https://github.com/bnbong/FastAPI-fastkit/blob/main/src/fastapi_fastkit/fragments/README.md)。 + ### 文档命令 | 命令 | 描述 | @@ -353,13 +374,16 @@ FastAPI-fastkit/ - **`src/fastapi_fastkit/`** —— 主包源码 - **`cli.py`** —— 主 CLI 入口 - **`backend/`** —— 核心后端逻辑 - - **`inspector.py`** —— 模板检查器 + - **`inspection/`** —— 模板检查流水线(context、checks、consistency、freshness、smoke、lint、report) + - **`inspector.py`** —— `inspection/` 之上的外观层(facade),为保留历史导入路径而存在 - **`interactive/`** —— 交互模式相关组件(提示、选择器、校验器) - **`package_managers/`** —— 包管理器实现(pip、uv、pdm、poetry) - - **`project_builder/`** —— 项目构建工具 + - **`project_builder/`** —— 项目构建工具,包括由片段驱动的配置生成器 + - **`scaffolder.py`** —— `init` 与 `startdemo` 共用的生成流水线 - **`transducer.py`** —— 模板 transducer - **`core/`** —— 核心配置与异常 - - **`fastapi_project_template/`** —— 项目模板(fastapi-default、fastapi-async-crud 等) + - **`fragments/`** —— 渲染到生成项目中的 Jinja2 片段(见上文) + - **`fastapi_project_template/`** —— 项目模板(fastapi-default、fastapi-domain-starter 等) - **`utils/`** —— 共享工具函数 - **`tests/`** —— 测试套件 - **`test_backends/`** —— 针对后端的测试 diff --git a/docs/zh/contributing/template-creation-guide.md b/docs/zh/contributing/template-creation-guide.md index ae3ebe9..f63a924 100644 --- a/docs/zh/contributing/template-creation-guide.md +++ b/docs/zh/contributing/template-creation-guide.md @@ -97,8 +97,7 @@ fastapi-{template-name}/ │ ├── 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) +├── pyproject.toml-tpl # ✅ Primary metadata (PEP 621, required) ├── requirements.txt-tpl # 🟡 Optional when pyproject declares deps ├── setup.cfg-tpl # Development tools configuration ├── README.md-tpl # ✅ Project documentation (required) @@ -110,10 +109,16 @@ fastapi-{template-name}/ - `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`(PEP 621) +- 在 `pyproject.toml-tpl` 的 `[project].dependencies` 或 `requirements.txt-tpl` 中声明 `fastapi` 依赖 -当 `pyproject.toml-tpl` 已声明 `[project].dependencies` 时,`requirements.txt-tpl` 不再是严格必需的。新模板**建议**采用 `pyproject.toml-tpl` 作为主要元数据文件。 +当 `pyproject.toml-tpl` 已声明 `[project].dependencies` 时,`requirements.txt-tpl` 不是必需的 —— 但如果模板同时附带两者,它们必须保持一致:两者之间存在依赖漂移时检查会失败。 + +**新模板以 pyproject 为先,不得附带 `setup.py-tpl`。**截至 v1.4.0,没有任何内置模板包含该文件。检查仍然接受该文件,以便仓库外的模板继续可用,但这是一条遗留路径。 + +**统一以 Python 3.12 为目标**:`requires-python = ">=3.12"`、black 的 `target-version = ["py312"]`、mypy 的 `python_version = "3.12"`,以及任何 Docker 基础镜像都使用 `python:3.12-slim`。检查会确认这三处固定版本保持一致。 + +**切勿将 `FastAPI-fastkit` 声明为生成项目的运行时依赖。**该 CLI 是开发工具;模板的依赖列表只应包含生成的应用本身会导入的内容。 ### 文件编写指南 @@ -237,52 +242,23 @@ isort==5.12.0 mypy==1.7.1 ``` -#### 4. 编写 setup.py-tpl(遗留 —— 在已有 pyproject 时为可选) +#### 4. 路由锚点 -保留它是为了兼容遗留模板。如果新模板已附带 `pyproject.toml-tpl`,则无需此文件。 +附带 `main.py-tpl` 或 API 聚合模块的模板,应当标记出 `fastkit addroute` 插入代码的位置: ```python -""" - package setup +from src.app.api import health -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", -] +# fastkit:imports -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", - ], -) +api_router = APIRouter() +api_router.include_router(health.router) + +# fastkit:routes ``` +如果没有这些锚点,`addroute` 会退回到基于 AST 的插入方式,自行定位 import 块和路由注册位置 —— 因此缺少锚点的模板仍然可以工作,但结果的可预测性较差。编辑这些文件时请保留这些注释。 + #### 5. 编写测试文件 ```python @@ -342,8 +318,17 @@ make inspect-template TEMPLATES="fastapi-your-template" # Validate with verbose output python scripts/inspect-templates.py --templates "fastapi-your-template" --verbose + +# Fast local loop: no network, no server boot +python scripts/inspect-templates.py --templates "fastapi-your-template" \ + --offline --no-smoke + +# Full check before opening a PR (adds mypy over the generated project) +python scripts/inspect-templates.py --templates "fastapi-your-template" --mypy ``` +`--offline` 会跳过 PyPI 依赖新鲜度查询,`--no-smoke` 会跳过启动生成项目(最慢的步骤),`--mypy` 会对生成项目进行类型检查。每项检查具体做什么,请参阅 [模板质量保障](../reference/template-quality-assurance.md)。 + !!! note 您提交 PR 后,**Template PR Inspection** 工作流会自动运行并校验您的模板改动,反馈会直接出现在 PR 上。 @@ -356,7 +341,7 @@ inspector 会自动校验以下项目: - [ ] 存在 `tests/` 目录 - [ ] 存在 `README.md-tpl` 文件 -- [ ] 至少存在 `pyproject.toml-tpl`(推荐)或 `setup.py-tpl`(遗留)之一 +- [ ] 存在 `pyproject.toml-tpl`(新模板中不包含 `setup.py-tpl`) #### ✅ 文件扩展名校验 @@ -368,7 +353,8 @@ inspector 会自动校验以下项目: - [ ] `fastapi` 至少声明于以下其一: - [ ] `pyproject.toml-tpl` 的 `[project].dependencies`(推荐) - [ ] `requirements.txt-tpl` - - [ ] `setup.py-tpl` 的 `install_requires` +- [ ] `requirements.txt-tpl` 与 `[project].dependencies` 不存在冲突 +- [ ] `FastAPI-fastkit` **不是**运行时依赖 #### ✅ FastAPI 实现校验 @@ -381,6 +367,24 @@ inspector 会自动校验以下项目: - [ ] 成功安装依赖 - [ ] 所有 pytest 测试通过 +#### ✅ 冒烟测试 + +- [ ] 生成的项目可以在 uvicorn 下启动 +- [ ] `/docs` 有响应 +- [ ] `/health` 有响应 + +uvicorn 的目标依次从生成的 `pyproject.toml` 中的 `[tool.fastapi-fastkit].app_module`、`template-config.yml` 中的 `app_module`,以及约定俗成的 `main.py` 位置解析得到。 + +#### ✅ 配置一致性 + +- [ ] `requires-python`、black 的 `target-version` 与 mypy 的 `python_version` 都指向 Python 3.12 +- [ ] `requirements.txt-tpl` 与 `[project].dependencies` 之间不存在依赖漂移 +- [ ] 不存在对 `FastAPI-fastkit` 的自我依赖 + +#### ✅ 占位符残留 + +- [ ] 项目生成后不残留任何 `` + #### ✅ 自动化模板测试 FastAPI-fastkit 自带**自动化模板测试**,会对所有模板执行完整测试: diff --git a/docs/zh/index.md b/docs/zh/index.md index 759a027..b6835a6 100644 --- a/docs/zh/index.md +++ b/docs/zh/index.md @@ -34,6 +34,8 @@ - **🔍 自动化的模板质量保障**:通过每周自动化测试,确保所有模板始终可用并保持更新 - **🚀 多种项目模板**:提供多种预配置模板,覆盖 async CRUD、Docker、PostgreSQL 等不同使用场景 - **📦 支持多种包管理器**:可选用您偏好的 Python 包管理器(pip、uv、pdm、poetry)管理依赖 +- **🧾 可复现的项目配置**:用 `--save-config` 保存一次交互式会话,之后用 `fastkit init --config` 重放 —— 无需任何提示,得到相同的项目 +- **🏷️ 自描述的项目**:每个生成的项目都会在 `[tool.fastapi-fastkit]` 块中记录其模板、预设、包管理器、入口和所选功能,`runserver` 与 `addroute` 会读回这些信息 ## 安装 @@ -123,7 +125,7 @@ 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 │ +│ ℹ Injected metadata into pyproject.toml │ ╰──────────────────────────────────────────────────────╯ ╭──────────────────────── Info ────────────────────────╮ │ ℹ Injected metadata into config file │ @@ -174,6 +176,16 @@ Installing dependencies... 该命令将创建一个包含 Python 虚拟环境的新 FastAPI 项目工作区。 +- 主要选项: + - `--project-name`、`--author`、`--author-email`、`--description` + - `--package-manager` [pip|uv|pdm|poetry] + - `--config `:根据已保存的配置文件(`.json` / `.toml`,安装了 PyYAML 时还支持 `.yaml`)创建项目,不进行任何提示 + - `--save-config `:与 `--interactive` 一起使用,记录回答以便之后复用 + - `--dry-run`:打印将要创建的文件和包,不写入任何内容 + - `--no-venv`:跳过虚拟环境创建(隐含 `--no-install`) + - `--no-install`:跳过依赖安装 + - `--yes` / `-y`:原地部署时跳过 "Overwrite these files?" 确认 + ### 使用交互模式创建项目 ✨ 新功能 如果项目需求更复杂,可以使用 **交互模式** 通过功能选择一步步搭建 FastAPI 应用: @@ -264,6 +276,30 @@ Select utilities (comma-separated numbers, e.g., 1,3,4): Select utilities: 1 +🪵 Logging Format +Select logging format: + 1. structured - JSON logs + X-Request-ID correlation middleware (stdlib only) + 2. None - Uvicorn's default plain-text logging + +Select logging format [2]: 1 + +🧬 Database Migrations +Select migration tool: + 1. Alembic - alembic.ini + async alembic/env.py + scripts/migrate.sh (recommended for SQL databases) + 2. None - No migration tooling + +Select migration tool [1]: 1 + +🧰 Developer Tooling +Select tooling to generate (comma-separated numbers, e.g., 1,3) + 1. ruff ruff + ruff-format config in pyproject.toml (replaces black/isort) + 2. pre-commit .pre-commit-config.yaml + 3. github-actions .github/workflows/test.yml (Python 3.12) + 4. devcontainer .devcontainer/devcontainer.json + 5. makefile Makefile with install/test/lint/format/run + +Your choice (or press Enter to skip): 1,2 + 🚀 Deployment Configuration Select deployment option: 1. Docker - Generate Dockerfile @@ -294,6 +330,9 @@ Enter custom package names (comma-separated, press Enter to skip): │ Monitoring │ Prometheus │ │ Testing │ Coverage │ │ Utilities │ CORS │ +│ Logging │ structured │ +│ Migrations │ Alembic │ +│ Tooling │ ruff, pre-commit │ │ Package Manager │ uv │ └─────────────────────┴───────────────────────────────────────────────────────────────────────────┘ @@ -350,6 +389,16 @@ Installing dependencies... - **智能依赖管理**,自动兼容 pip - **功能校验**,当预设无法自动完成接入时会给出手动处理提示 - **项目身份标识** 会写入生成的 `pyproject.toml`(description 标识 + `[tool.fastapi-fastkit]` 节),便于后续由 `is_fastkit_project()` 识别生成的项目 +- **项目元数据** 记录在 `[tool.fastapi-fastkit]` 中 —— 模板、预设、包管理器、`app_module` 以及所选的 `features` —— `fastkit runserver` 与 `fastkit addroute` 会读回这些信息 +- **可复现的运行**:`--save-config ` 记录回答,`--config ` 在不出现任何提示的情况下重放它们,`--dry-run` 则在不写入任何内容的情况下预览结果 + +现在目录中的每个选择都会生成真正可运行的代码,而不仅仅是安装一个包 —— 后台任务(Celery/Dramatiq)、Redis 缓存、WebSocket、分页、OpenTelemetry 追踪,以及 OAuth2 / 基于会话的认证都附带对应的模块和 `main.py` 接入。另有三个新维度补全了这份目录: + +- **日志**:`structured` 添加基于标准库 `logging` + `json` 的结构化输出以及 request-id 中间件 —— 不引入新依赖 +- **迁移**:只要选择了任意 SQL 数据库,`Alembic` 就会生成完整的异步迁移环境(`alembic.ini`、`alembic/env.py`、一个基线修订版本、`scripts/migrate.sh`) +- **工具链**(多选):`ruff`、`pre-commit`、`github-actions`、`devcontainer`、`makefile` —— 各自生成对应的配置文件;`ruff` 还会把 `[tool.ruff]` 块合并进 `pyproject.toml` + +每个生成的项目还会获得 `/health` 与 `/ready` 端点,而 `--dry-run` 列出的文件集合与真实运行写入的完全相同,包括这些新产物。`classic-layered` 与 `domain-starter` 会保留其自带的 `main.py`,因此当上述任一维度下的选择需要手动接入时,功能校验会给出警告 —— 详情请参阅[预设 / 功能矩阵](reference/preset-feature-matrix.md)。 ### 向 FastAPI 项目添加新路由 @@ -445,6 +494,12 @@ FastAPI template project will deploy at '~your-project-path~'
+- 主要选项: + - `--project-name`、`--author`、`--author-email`、`--description` + - `--package-manager` [pip|uv|pdm|poetry] + - `--dry-run`、`--no-venv`、`--no-install`、`--yes`/`-y` —— 含义与 `init` 中相同 + - 如果目标项目目录已经存在,则拒绝运行(`--dry-run` 除外) + 如需查看可用的 FastAPI 示例列表,请运行:
@@ -463,18 +518,42 @@ $ fastkit list-templates │ │ PostgreSQL │ │ fastapi-default │ Simple FastAPI Project │ │ fastapi-single-module │ FastAPI Single Module Template │ +│ fastapi-auth-jwt │ FastAPI JWT Authentication │ +│ fastapi-sqlmodel │ FastAPI SQLModel │ +│ fastapi-llm-agent │ FastAPI LLM Agent │ └────────────────────────┴───────────────────────────────────────────────────────┘ ```
+可用的模板: + +| 模板 | 描述 | 备注 | +|---|---|---| +| [`fastapi-default`](https://github.com/bnbong/FastAPI-fastkit/blob/main/src/fastapi_fastkit/fastapi_project_template/fastapi-default/README.md-tpl) | 采用经典分层布局的简单 FastAPI 项目 | 适合作为第一个选择 | +| [`fastapi-empty`](https://github.com/bnbong/FastAPI-fastkit/blob/main/src/fastapi_fastkit/fastapi_project_template/fastapi-empty/README.md-tpl) | 最小化的 FastAPI 模板 | `minimal` 预设的基础 | +| [`fastapi-single-module`](https://github.com/bnbong/FastAPI-fastkit/blob/main/src/fastapi_fastkit/fastapi_project_template/fastapi-single-module/README.md-tpl) | 单文件 FastAPI 应用 | `single-module` 预设的基础 | +| [`fastapi-domain-starter`](https://github.com/bnbong/FastAPI-fastkit/blob/main/src/fastapi_fastkit/fastapi_project_template/fastapi-domain-starter/README.md-tpl) | 面向中型 API、以 pyproject 为先的面向领域 starter | `domain-starter` 预设的基础 | +| [`fastapi-auth-jwt`](https://github.com/bnbong/FastAPI-fastkit/blob/main/src/fastapi_fastkit/fastapi_project_template/fastapi-auth-jwt/README.md-tpl) | 带刷新令牌轮换、argon2 哈希与作用域的 JWT 认证 | v1.4.0 新增 | +| [`fastapi-sqlmodel`](https://github.com/bnbong/FastAPI-fastkit/blob/main/src/fastapi_fastkit/fastapi_project_template/fastapi-sqlmodel/README.md-tpl) | 异步 SQLModel + Alembic 迁移 + 带分页的通用 CRUD | v1.4.0 新增 | +| [`fastapi-llm-agent`](https://github.com/bnbong/FastAPI-fastkit/blob/main/src/fastapi_fastkit/fastapi_project_template/fastapi-llm-agent/README.md-tpl) | 带工具调用循环的流式 Claude 聊天智能体(SSE) | v1.4.0 新增 | +| [`fastapi-mcp`](https://github.com/bnbong/FastAPI-fastkit/blob/main/src/fastapi_fastkit/fastapi_project_template/fastapi-mcp/README.md-tpl) | 以 MCP 服务器形式暴露的 FastAPI 应用 | | +| [`fastapi-psql-orm`](https://github.com/bnbong/FastAPI-fastkit/blob/main/src/fastapi_fastkit/fastapi_project_template/fastapi-psql-orm/README.md-tpl) | 基于 PostgreSQL、SQLModel、Alembic 和 docker-compose 的 item 管理 API | 需要 Docker | +| [`fastapi-custom-response`](https://github.com/bnbong/FastAPI-fastkit/blob/main/src/fastapi_fastkit/fastapi_project_template/fastapi-custom-response/README.md-tpl) | 带自定义响应封装、错误处理和分页的 item 管理 API | | +| [`fastapi-async-crud`](https://github.com/bnbong/FastAPI-fastkit/blob/main/src/fastapi_fastkit/fastapi_project_template/fastapi-async-crud/README.md-tpl) | 异步 item 管理 API | **已弃用** —— 请使用 `fastapi-sqlmodel` | +| [`fastapi-dockerized`](https://github.com/bnbong/FastAPI-fastkit/blob/main/src/fastapi_fastkit/fastapi_project_template/fastapi-dockerized/README.md-tpl) | 已 Docker 化的 item 管理 API | **已弃用** —— 请使用 `fastapi-default` 并在 `init --interactive` 中选择 Docker 选项 | + +已弃用的模板仍然可用,但不再推荐作为起点。选型指南请参阅[应该选择哪个 starter?](user-guide/choosing-a-starter.md)。 + ## 文档 如需更完整的指南与详细使用说明,请查阅我们的文档: +- 🧭 **[应该选择哪个 starter?](user-guide/choosing-a-starter.md)** —— 面向新手的 `startdemo` 模板与交互式预设选型指南 - 📚 **[用户指南](user-guide/quick-start.md)** —— 详细的安装与使用指南 - 🎯 **[教程](tutorial/getting-started.md)** —— 面向新手的分步教程 - 📖 **[CLI 参考](user-guide/cli-reference.md)** —— 完整的命令参考 +- 🧱 **[架构预设矩阵](reference/preset-feature-matrix.md)** —— 交互式生成中按预设 / 按功能划分的契约 - 🔍 **[模板质量保障](reference/template-quality-assurance.md)** —— 自动化测试与质量标准 ## 🚀 基于模板的教程 @@ -486,16 +565,19 @@ $ fastkit list-templates - **[构建基础 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,这是推荐的现代默认选项 +- **[JWT 认证](tutorial/auth-jwt.md)** —— 借助 `fastapi-auth-jwt` 模板添加真实账户:轮换式刷新令牌、argon2id 哈希、角色与作用域守卫 ### 🗄️ 数据库与基础设施 - **[集成数据库](tutorial/database-integration.md)** —— 借助 `fastapi-psql-orm` 模板使用 PostgreSQL + SQLAlchemy -- **[Docker 化与部署](tutorial/docker-deployment.md)** —— 借助 `fastapi-dockerized` 模板搭建生产部署环境 +- **[使用 SQLModel 实现异步持久化](tutorial/sqlmodel.md)** —— 借助 `fastapi-sqlmodel` 模板实现异步 SQLModel、Alembic 迁移、通用 CRUD 基类与分页列表 +- **[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 服务器 +- **[流式 LLM 智能体](tutorial/llm-agent.md)** —— 借助 `fastapi-llm-agent` 模板构建带有真正工具调用循环的流式 Claude 聊天后端 每个教程包含: diff --git a/docs/zh/reference/preset-feature-matrix.md b/docs/zh/reference/preset-feature-matrix.md index 46fc712..ac90257 100644 --- a/docs/zh/reference/preset-feature-matrix.md +++ b/docs/zh/reference/preset-feature-matrix.md @@ -29,12 +29,18 @@ | 功能 | `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 覆盖。 | 同上。 | +| **认证**(JWT、FastAPI-Users、OAuth2、基于会话) | 生成认证配置模块;JWT 会导入 `HTTPBearer`,OAuth2 与基于会话的认证还会添加各自的 `main.py` 中间件配置。 | 在预设对应路径生成认证配置模块。不会向 `main.py` 添加 import —— 需要手动接入依赖。 | +| **后台任务**(Celery、Dramatiq) | 生成 `worker.py` 与 `features/tasks.py`,并在重新生成的 `main.py` 中挂载任务路由器。 | 生成 `worker.py` 与 `features/tasks.py`,但自带的 `main.py` 会**保留** —— 需要手动挂载路由器。 | +| **缓存**(Redis) | 生成 `features/cache.py`,并在重新生成的 `main.py` 中挂载并初始化。 | 生成 `features/cache.py`;需要手动挂载。 | +| **工具类:分页 / WebSocket** | 生成 `features/pagination.py` / `features/websocket.py`,并在重新生成的 `main.py` 中挂载路由器。 | 生成相同的模块;需要手动挂载。 | +| **监控:OpenTelemetry** | 不生成独立文件 —— 追踪配置直接加入重新生成的 `main.py`。 | 不生成独立文件。自带的 `main.py` 会保留,因此追踪**不会被接入** —— 需要手动添加配置。 | +| **迁移**(Alembic) | 只要选择了任意 SQL 数据库,就会生成 `alembic.ini`、`alembic/env.py`、一个基线修订版本和 `scripts/migrate.sh` —— 与 `main.py` 覆盖无关。 | 同上 —— Alembic 的生成不取决于 `main.py` 是否被保留。 | +| **工具链**(ruff、pre-commit、github-actions、devcontainer、makefile) | 生成对应的配置文件,与 `main.py` 覆盖无关。 | 同上。 | +| **日志**(structured) | 生成 `logging_config.py`,并把 request-id 中间件接入重新生成的 `main.py`。 | 生成 `logging_config.py`;自带的 `main.py` 会保留,因此需要手动接入该中间件。 | | **CORS**(工具类) | 在重新生成的 `main.py` 中加入 `CORSMiddleware`,`allow_origins=['*']`。 | 自带的 `main.py` 中**已接入**(以 `settings.all_cors_origins` 作为条件)。只需在 `.env` 中设置 `BACKEND_CORS_ORIGINS` 即可启用 —— 无需修改代码。 | -| **测试**(基础 / 覆盖率 / 进阶) | 在项目根生成 `pytest.ini`。 | 同上。 | +| **测试**(基础 / 覆盖率 / 进阶) | 在项目根生成 `pytest.ini`;进阶级别还会添加 `tests/factories.py` 与 `tests/test_factories.py`。 | 同上。 | | **部署**(Docker、docker-compose) | 在项目根写入 `Dockerfile` 和/或 `docker-compose.yml`。 | 同上。 | +| **`/health` / `/ready`** | 重新生成的 `main.py` 中始终包含。 | 保留下来的模板自带 `main.py` 中已经包含。 | ## 何时会出现「Preset compatibility」警告 @@ -44,10 +50,15 @@ |---|---| | `CORS`(工具类) | ❌ —— 自带的 `main.py` 已接入。只需在 `.env` 中填好 `BACKEND_CORS_ORIGINS`。 | | `Rate-Limiting`(工具类) | ✅ —— 不会加入 `slowapi` 限流器的初始化 | -| `Prometheus`(监控) | ✅ —— 不会调用 `Instrumentator().instrument(app)` | +| `Pagination` / `WebSocket`(工具类) | ✅ —— 生成的路由器(`features/pagination.py` / `features/websocket.py`)不会被挂载 | +| `Celery` / `Dramatiq`(async_tasks) | ✅ —— 生成的 `worker.py` / `features/tasks.py` 路由器不会被挂载 | +| `Redis`(缓存) | ✅ —— 生成的 `features/cache.py` 不会被挂载或初始化 | +| `Prometheus` / `OpenTelemetry`(监控) | ✅ —— 不会向 `main.py` 添加监控埋点配置 | +| `structured`(日志) | ✅ —— 会生成 `logging_config.py`,但不会向 `main.py` 添加 request-id 中间件 | +| `Alembic`(迁移)、`tooling` 选择 | ❌ —— 这些文件无论预设如何都会生成且可直接使用;不依赖 `main.py` | | 任意数据库 / 认证选择 | ⚠️ —— 配置文件已生成,但需要您手动在路由器中通过 `Depends()` 接入 | -对 `minimal` 与 `single-module` 预设,动态的 `main.py` 覆盖会自动处理 CORS、限流与 Prometheus 监控,因此不会触发警告。 +对 `minimal` 与 `single-module` 预设,动态的 `main.py` 覆盖会自动处理上述所有方面,因此不会触发警告。 ## 不支持的组合(安全起见) diff --git a/docs/zh/reference/template-quality-assurance.md b/docs/zh/reference/template-quality-assurance.md index f6ffa83..4e55022 100644 --- a/docs/zh/reference/template-quality-assurance.md +++ b/docs/zh/reference/template-quality-assurance.md @@ -21,6 +21,38 @@ FastAPI-fastkit 同时使用**两套互补的质量保障系统**: - ✅ **依赖检查** —— 确认 FastAPI 及必要依赖被正确声明 - ✅ **FastAPI 实现** —— 确认模板包含合规的 FastAPI 应用初始化 - ✅ **测试执行** —— 运行模板的测试以确认功能正常 +- ✅ **冒烟测试** —— 用 uvicorn 启动生成的项目,并探测其 HTTP 接口 +- ✅ **配置一致性** —— Python 版本固定、依赖漂移、自我依赖 +- ✅ **占位符残留** —— 项目生成后不残留任何 `` +- ✅ **依赖新鲜度** —— 对落后于 PyPI 最新发布版本的固定版本发出警告 + +## 各项检查的作用 + +检查会从模板生成一个**真实的项目** —— 复制、转换 `-tpl`、注入元数据 —— 然后针对这个生成的项目而不是模板源码运行每一项检查。只读取模板的检查无法发现生成过程中未能替换的占位符。 + +### 冒烟测试 + +生成的项目会在一个空闲端口上用 uvicorn 启动,检查器会持续轮询,直到服务器响应或超时。服务器启动后,会请求 `/docs` 与 `/health` 并检查其状态码。随后终止服务器进程,如果它没有停止,则升级为 `SIGKILL`。 + +uvicorn 的目标按以下顺序解析:生成的 `pyproject.toml` 中的 `[tool.fastapi-fastkit].app_module`,然后是模板 `template-config.yml` 中的 `app_module`,最后是约定俗成的 `main.py` 位置。这与 `fastkit runserver` 使用的契约相同,因此冒烟测试能够启动的模板,其生成的项目也能够运行。 + +这项检查能发现静态分析发现不了的问题:只在运行时才解析的 import、会抛出异常的 `lifespan`、没有默认值的配置字段。 + +### 配置一致性 + +会针对模板的 `pyproject.toml-tpl` 验证三件事: + +- **Python 版本固定保持一致并以 3.12 为目标** —— `requires-python`、black 的 `target-version` 与 mypy 的 `python_version`。一个声明 `>=3.12` 却按 `py311` 格式化的模板,就是一个迟早会坑到贡献者的 bug。 +- **没有依赖漂移** —— `requirements.txt-tpl` 与 `[project].dependencies` 中声明的包不得相互冲突。模板为了兼容各种包管理器会同时附带这两个文件,而两份列表如果没有检查,总会逐渐偏离。 +- **没有自我依赖** —— 生成的项目不得将 `FastAPI-fastkit` 声明为运行时依赖。该 CLI 是开发工具;把它打包进每个生成的应用,会把整个 fastkit 依赖树带入无关的项目。 + +### 占位符残留 + +每个 ``、`` 及同类占位符都必须在生成过程中被替换。生成的项目中仍然存在任何 `` 都会被视为错误 —— 这意味着元数据注入不知道那个文件,用户会原样收到它。 + +### 依赖新鲜度 + +固定的版本会与 PyPI 上的最新发布版本进行比较,并以警告而非错误的形式报告 —— 一个落后一个小版本的模板只是一条信息,而不是一次失败。这是唯一一项会访问网络的检查,`--offline` 会跳过它。 ## 自动化模板测试系统 @@ -140,6 +172,30 @@ $ python scripts/inspect-templates.py --verbose $ python scripts/inspect-templates.py --output my_results.json ``` +#### 检查选项 + +| 选项 | 作用 | 适用场景 | +|---|---|---| +| `--offline` | 跳过所有网络查询(依赖新鲜度报告) | 没有网络,或者想要快速、封闭的运行 | +| `--no-smoke` | 跳过用 uvicorn 启动生成的项目 | 只在迭代结构;冒烟测试是最慢的步骤 | +| `--mypy` | 用 mypy 对生成的项目进行类型检查 | 提交模板之前 —— 由于速度较慢,需要显式开启 | +| `--templates` | 要检查的模板列表,以逗号分隔 | 只处理一个模板时 | +| `--verbose` | 详细输出 | 调试失败时 | +| `--output` | JSON 结果文件的路径 | 需要并排保留多次运行结果时 | + +针对单个模板的快速本地循环: + +```console +$ python scripts/inspect-templates.py \ + --templates fastapi-sqlmodel --offline --no-smoke --verbose +``` + +……以及提交 PR 之前的完整检查: + +```console +$ python scripts/inspect-templates.py --templates fastapi-sqlmodel --mypy +``` + ### 使用 Makefile 命令 ```console @@ -192,10 +248,11 @@ $ make inspect-template TEMPLATES="fastapi-default,fastapi-async-crud" - 必须包含一个带 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` 为可选 +- 必须包含 `pyproject.toml-tpl`(PEP 621)。内置模板以 pyproject 为先,不再附带 `setup.py-tpl`;检查仍然接受该文件,以便第三方模板继续可用,但新模板不得添加它。 +- 当 `pyproject.toml-tpl` 已声明 `[project].dependencies` 时,`requirements.txt-tpl` 为可选 —— 但如果两者同时存在,它们必须保持一致(参见*配置一致性*) +- 必须统一以 Python 3.12 为目标:`requires-python = ">=3.12"`、black 的 `target-version = ["py312"]`、mypy 的 `python_version = "3.12"`,以及任何 Docker 基础镜像都使用 `python:3.12-slim` +- 不得将 `FastAPI-fastkit` 声明为运行时依赖 +- 生成后不得留下任何未替换的 `` ### FastAPI 要求 - 必须包含 FastAPI 应用初始化 diff --git a/docs/zh/reference/translation-status.md b/docs/zh/reference/translation-status.md index ea9d6ef..3088092 100644 --- a/docs/zh/reference/translation-status.md +++ b/docs/zh/reference/translation-status.md @@ -18,15 +18,15 @@ FastAPI-fastkit 以多种语言发布文档,但这些翻译**并不总是完全 | 语言 | 状态 | 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`。 | +| 🇬🇧 英文(`en`) | ✅ 权威来源 | 29 / 29 | 标准来源。 | +| 🇰🇷 韩文(`ko`) | ✅ 完成 | 29 / 29 | Phase 1-3 已完成。已于 2026-10-11 与 v1.4.1 的英文源重新同步:v1.4.0 新增的三篇教程(`tutorial/auth-jwt.md`、`tutorial/sqlmodel.md`、`tutorial/llm-agent.md`)已经翻译,自最初翻译以来与英文产生偏差的页面也已重新对齐。`docs/ko/changelog.md` 刻意复用规范的英文 `CHANGELOG.md`。 | +| 🇯🇵 日文(`ja`) | ✅ 完成 | 29 / 29 | Phase 1-3 已完成。已于 2026-10-11 与 v1.4.1 的英文源重新同步:v1.4.0 新增的三篇教程(`tutorial/auth-jwt.md`、`tutorial/sqlmodel.md`、`tutorial/llm-agent.md`)已经翻译,自最初翻译以来与英文产生偏差的页面也已重新对齐。`docs/ja/changelog.md` 刻意复用规范的英文 `CHANGELOG.md`。 | +| 🇨🇳 中文(`zh`) | ✅ 完成 | 29 / 29 | Phase 1-3 已完成。已于 2026-10-11 与 v1.4.1 的英文源重新同步:v1.4.0 新增的三篇教程(`tutorial/auth-jwt.md`、`tutorial/sqlmodel.md`、`tutorial/llm-agent.md`)已经翻译,自最初翻译以来与英文产生偏差的页面也已重新对齐。`docs/zh/changelog.md` 刻意复用规范的英文 `CHANGELOG.md`。 | +| 🇪🇸 西班牙文(`es`) | ✅ 完成 | 29 / 29 | Phase 1-3 已完成。已于 2026-10-11 与 v1.4.1 的英文源重新同步:v1.4.0 新增的三篇教程(`tutorial/auth-jwt.md`、`tutorial/sqlmodel.md`、`tutorial/llm-agent.md`)已经翻译,自最初翻译以来与英文产生偏差的页面也已重新对齐。`docs/es/changelog.md` 刻意复用规范的英文 `CHANGELOG.md`。 | +| 🇫🇷 法文(`fr`) | ✅ 完成 | 29 / 29 | Phase 1-3 已完成。已于 2026-10-11 与 v1.4.1 的英文源重新同步:v1.4.0 新增的三篇教程(`tutorial/auth-jwt.md`、`tutorial/sqlmodel.md`、`tutorial/llm-agent.md`)已经翻译,自最初翻译以来与英文产生偏差的页面也已重新对齐。`docs/fr/changelog.md` 刻意复用规范的英文 `CHANGELOG.md`。 | +| 🇩🇪 德文(`de`) | ✅ 完成 | 29 / 29 | Phase 1-3 已完成。已于 2026-10-11 与 v1.4.1 的英文源重新同步:v1.4.0 新增的三篇教程(`tutorial/auth-jwt.md`、`tutorial/sqlmodel.md`、`tutorial/llm-agent.md`)已经翻译,自最初翻译以来与英文产生偏差的页面也已重新对齐。`docs/de/changelog.md` 刻意复用规范的英文 `CHANGELOG.md`。 | -*快照验证时间:2026-06-18;在 Phase 3(贡献文档 + 参考文档)完成后,已基于当前分支重新统计 `zh` 这一行。中文目前 26 个本地化页面全部就位,状态记为 ✅ 完成。* 此表通过人工维护;如需在仓库根目录重新统计当前状态,请运行: +*快照验证时间:2026-10-11,v1.4.1 发布之后。全部六个翻译语言都已与英文源达到一致:v1.4.0 新增的三篇教程已经翻译,所有自最初翻译以来出现偏差的页面也都已重新同步。在一个版本周期内,翻译仍可能暂时落后 —— 如果某个翻译页面与英文不一致,请以英文为准。* 此表通过人工维护;如需在仓库根目录重新统计当前状态,请运行: ```console $ for loc in en ko ja zh es fr de; do @@ -39,6 +39,7 @@ $ for loc in en ko ja zh es fr de; do 图例: - ✅ **权威来源** —— 我们以此语言为基准撰写。 +- ✅ **完成** —— 每个英文页面都有对应的翻译页面。在一个版本周期内,个别页面仍可能落后于英文的更新。 - 🟡 **部分翻译** —— 已翻译部分页面;缺失的页面会回退到英文。 - 🔴 **骨架** —— 语言切换器中存在该入口,但尚未提交任何翻译过的页面。站点会在翻译后的导航标签下渲染英文内容。 diff --git a/docs/zh/tutorial/auth-jwt.md b/docs/zh/tutorial/auth-jwt.md new file mode 100644 index 0000000..61be261 --- /dev/null +++ b/docs/zh/tutorial/auth-jwt.md @@ -0,0 +1,249 @@ +# 使用 `fastapi-auth-jwt` 实现 JWT 认证 + +构建一个从第一次提交起就拥有真实账户的 API。本教程会端到端走完 `fastapi-auth-jwt` 模板:令牌对如何签发与轮换、为什么被重放的刷新令牌会被拒绝、角色与作用域(scope)守卫如何接入,以及从 SQLite 迁移出去时需要改动什么。 + +## 您将学到的内容 + +- 使用 `fastkit startdemo fastapi-auth-jwt` 生成项目 +- 访问令牌与刷新令牌的区别,以及轮换为何重要 +- `get_current_user`、`get_current_active_superuser` 与 `require_scopes(...)` 如何保护路由 +- 密码哈希在哪里进行,以及为什么使用 argon2id +- 从自带的 SQLite 文件迁移到配合 Alembic 的 PostgreSQL + +## 前置条件 + +- Python 3.12+ +- 已安装 FastAPI-fastkit(`pip install fastapi-fastkit`) +- 熟悉 FastAPI 依赖与 pydantic 模式 + +如果您从未构建过 FastAPI 应用,请先阅读 [构建基础 API 服务器](basic-api-server.md) 再回来 —— 本模板假定您已掌握基础知识。 + +## 第 1 步:生成项目 + +```console +$ fastkit startdemo fastapi-auth-jwt +Enter the project name: accounts-api +Enter the author name: Developer Kim +Enter the author email: developer@example.com +Enter the project description: Accounts and sessions service +Select package manager (pip, uv, pdm, poetry) [uv]: uv +Do you want to proceed with project creation? [y/N]: y +``` + +然后运行它: + +```console +$ cd accounts-api +$ bash scripts/run-server.sh # or: uvicorn src.app.main:app --reload +``` + +应用基于 SQLite(`sqlite:///./app.db`)启动,并在启动时创建数据表,因此无需事先安装或配置任何东西。文档位于 。 + +!!! tip "提交前先预览" + `fastkit startdemo fastapi-auth-jwt --dry-run` 会打印文件树和包列表,而不写入任何内容 —— 便于查看您即将得到什么。 + +## 第 2 步:生成的目录树 + +``` +accounts-api/ +├── pyproject.toml # PEP 621 metadata + [tool.fastapi-fastkit] +├── requirements.txt +├── alembic.ini +├── Dockerfile +├── .env # SECRET_KEY, DATABASE_URL, token lifetimes +├── scripts/ +│ ├── format.sh lint.sh run-server.sh test.sh +├── src/ +│ └── app/ +│ ├── main.py # FastAPI() + lifespan + CORS + api_router +│ ├── core/ +│ │ ├── config.py # pydantic-settings +│ │ └── security.py # argon2 hashing + JWT encode/decode +│ ├── db/session.py # engine + get_session dependency +│ ├── api/ +│ │ ├── router.py # aggregates health + every domain router +│ │ ├── health.py # GET /health +│ │ └── deps.py # session, current user, role/scope guards +│ ├── domains/ +│ │ ├── auth/ # login, refresh rotation, logout +│ │ └── users/ # registration and profiles +│ └── alembic/ # migration environment + versions/ +└── tests/ + ├── conftest.py test_health.py test_auth.py test_users.py +``` + +该布局采用 `domain-starter` 的结构:每个领域各自拥有 `router.py`(传输层)、`service.py`(业务逻辑)、`repository.py`(数据访问)、`models.py`(数据表)和 `schemas.py`(API 输入/输出)。认证相关的内容放在 `auth/`,账户相关的内容放在 `users/`,两者都不会深入对方的内部实现。 + +## 第 3 步:注册、登录并调用受保护的路由 + +```console +# 1. register +$ curl -X POST http://127.0.0.1:8000/api/v1/auth/register \ + -H 'Content-Type: application/json' \ + -d '{"email": "alice@example.com", "password": "s3cret-password"}' + +# 2. log in — note the form encoding: this is the OAuth2 password flow +$ curl -X POST http://127.0.0.1:8000/api/v1/auth/login \ + -d 'username=alice@example.com&password=s3cret-password' +{"access_token": "...", "refresh_token": "...", "token_type": "bearer"} + +# 3. call a protected route +$ curl http://127.0.0.1:8000/api/v1/users/me \ + -H "Authorization: Bearer $ACCESS_TOKEN" +``` + +Swagger 的 "Authorize" 按钮驱动的是同一流程,因此您无需离开浏览器即可在 `/docs` 中调用受保护的路由。 + +完整的端点一览: + +| 方法 | 端点 | 认证 | 说明 | +|--------|-----------------------------|-------------|-----------------------------------| +| GET | `/api/v1/health` | — | 存活探针 | +| POST | `/api/v1/auth/register` | — | 创建账户 | +| POST | `/api/v1/auth/login` | — | OAuth2 密码表单 → 令牌对 | +| POST | `/api/v1/auth/refresh` | 刷新令牌 | 轮换为新的令牌对 | +| POST | `/api/v1/auth/logout` | 刷新令牌 | 吊销一个刷新令牌 | +| POST | `/api/v1/auth/logout-all` | 访问令牌 | 吊销该用户的所有会话 | +| GET | `/api/v1/users/me` | 访问令牌 | 当前用户的资料 | +| PATCH | `/api/v1/users/me` | 访问令牌 | 更新当前用户的资料 | +| GET | `/api/v1/users/me/scopes` | 作用域 `me` | 受作用域限制的示例路由 | +| GET | `/api/v1/users` | 超级用户 | 受角色限制的用户列表 | + +## 第 4 步:两个令牌,以及原因 + +登录会返回一个**令牌对**: + +- **访问令牌**的有效期较短(`ACCESS_TOKEN_EXPIRE_MINUTES`,默认 30),随每个请求发送。由于有效期短,即使泄露也会很快失效。 +- **刷新令牌**的有效期较长(`REFRESH_TOKEN_EXPIRE_DAYS`,默认 14),只用于一件事:获取新的令牌对。 + +以上并无特别之处。本模板与一般 JWT 代码片段的不同之处在于,刷新令牌会被**追踪并轮换**。每个签发的刷新令牌都带有 `jti` 声明,而该 `jti` 会被写入数据库: + +```python +def issue_token_pair(self, user: User) -> TokenPair: + scopes = user.scope_list + access_token, _, _ = create_access_token(str(user.id), scopes) + refresh_token, jti, expires_at = create_refresh_token(str(user.id), scopes) + self.refresh_tokens.add(jti, user.id or 0, expires_at) + return TokenPair(access_token=access_token, refresh_token=refresh_token) +``` + +刷新时,提交的令牌会在新令牌对签发**之前**被吊销: + +```python +def rotate(self, refresh_token: str) -> TokenPair: + payload = decode_token(refresh_token, REFRESH_TOKEN_TYPE) + record = self.refresh_tokens.get_by_jti(str(payload.get("jti"))) + if record is None or record.revoked: + raise TokenError("refresh token is no longer valid") + ... + self.refresh_tokens.revoke(record) + return self.issue_token_pair(user) +``` + +动手试试:先刷新一次,然后再次发送*旧的*刷新令牌。 + +```console +$ curl -X POST http://127.0.0.1:8000/api/v1/auth/refresh \ + -H 'Content-Type: application/json' \ + -d "{\"refresh_token\": \"$REFRESH_TOKEN\"}" +# → new pair + +$ curl -X POST http://127.0.0.1:8000/api/v1/auth/refresh \ + -H 'Content-Type: application/json' \ + -d "{\"refresh_token\": \"$REFRESH_TOKEN\"}" +# → 401: the token died the moment it was used +``` + +正是这一特性,让被盗的刷新令牌从一个永久后门变成了可被察觉的、只能使用一次的失败。 + +登出也以同样方式工作,这也是普通 JWT 配置通常做不到的另一件事:`POST /auth/logout` 吊销一个令牌,`POST /auth/logout-all` 吊销属于该用户的所有刷新令牌。 + +## 第 5 步:密码 + +哈希处理位于 `src/app/core/security.py`,通过 `pwdlib` 使用 **argon2id** —— 这是当前推荐的默认方案,而不是遗留的 bcrypt 配置。除了验证的那一刻,路由和服务中的任何代码都不会接触明文密码,`hashed_password` 也从不出现在任何响应模式中。 + +这里您无需配置任何东西。您*必须*配置的只有签名密钥。 + +## 第 6 步:角色与作用域 + +本模板以 FastAPI 依赖的形式提供了三个守卫: + +```python +CurrentUser = Annotated[User, Depends(get_current_user)] +``` + +- **`get_current_user`** 解码 Bearer 访问令牌并加载用户,对未激活的账户返回 `403`。格式错误、已过期或指向已删除用户的令牌都会返回 `401`。 +- **`get_current_active_superuser`** 在此之上叠加 `is_superuser` 检查。`GET /api/v1/users` 使用了它。 +- **`require_scopes(...)`** 构建一个要求特定作用域的依赖: + +```python +from fastapi import Depends +from src.app.api.deps import require_scopes + +@router.get("/reports", dependencies=[Depends(require_scopes("reports"))]) +def read_reports() -> list[str]: + ... +``` + +作用域以逗号分隔字符串的形式存放在用户行上(`scopes: str`,通过 `scope_list` 属性读取),并会被复制到每个签发的令牌中。对于入门模板来说,这是刻意选择的最简单可行方案;当您的权限模型超出一个字符串的承载能力时,可以用角色表替换该列,同时保持依赖签名不变 —— 路由中的任何代码都无需改动。 + +## 第 7 步:添加受保护的领域 + +例如,要添加一个只有已登录用户才能访问的 `orders` 领域: + +1. 参照 `users/` 创建 `src/app/domains/orders/`(`models.py`、`schemas.py`、`repository.py`、`service.py`、`router.py`)。 +2. 在需要账户的路由中以参数形式接收 `CurrentUser`,或者为路由器附加 `Depends(require_scopes("orders"))`。 +3. 在 `src/app/api/router.py` 中、`# fastkit:routes` 锚点之上注册路由器。`fastkit addroute` 会写入同样的锚点,因此手写的路由与生成的路由会保持在同一处。 +4. 生成迁移:`alembic revision --autogenerate -m "add orders"`。 +5. 参照 `tests/test_users.py` 为新端点编写测试。 + +## 第 8 步:现在用 SQLite,以后用 PostgreSQL + +默认的 `DATABASE_URL` 是一个本地 SQLite 文件,应用会在启动时调用 `create_db_and_tables()`,这正是 `pytest` 和 `uvicorn` 无需任何配置即可运行的原因。对于本地开发以外的场景,请改由 Alembic 管理数据库模式: + +```console +$ export DATABASE_URL='postgresql+psycopg://postgres:postgres@localhost:5432/app_db' +$ alembic upgrade head +``` + +修改模型之后: + +```console +$ alembic revision --autogenerate -m "add profile fields" +``` + +## 第 9 步:部署之前 + +`.env` 中的两个设置决定了这是一个演示还是一个真正的服务: + +- **`SECRET_KEY`** 为每个令牌签名。如果不设置,每个进程都会生成一个随机密钥 —— 这会导致每次重启都让所有人登出,并让多个 worker 对哪些令牌有效产生分歧。请生成一个并固定下来: + + ```console + $ python -c "import secrets; print(secrets.token_urlsafe(32))" + ``` + +- **`DATABASE_URL`** 选择数据库。默认的 SQLite 不适合作为生产存储。 + +还可以选择设置 `FIRST_SUPERUSER_EMAIL` 和 `FIRST_SUPERUSER_PASSWORD`,以便在启动时创建一个超级用户;并将 `BACKEND_CORS_ORIGINS` 设置为以逗号分隔的前端源列表。 + +## 第 10 步:测试 + +```console +$ bash scripts/test.sh # or: pytest +``` + +测试套件使用 `httpx.AsyncClient` 针对内存中的 SQLite 数据库运行,覆盖了完整的正常路径 —— 注册 → 登录 → me → 刷新 → 登出 —— 以及真正重要的拒绝路径:错误密码、格式错误的令牌、过期令牌,以及第 4 步中被重放的刷新令牌。当您扩展认证规则时,请先扩展这些拒绝测试;能够发现守卫失效的正是它们。 + +## 回顾 + +- **生成**:`fastkit startdemo fastapi-auth-jwt` → `bash scripts/run-server.sh` → 在 `/docs` 查看文档。 +- **令牌**:短期访问令牌用于请求,轮换式刷新令牌通过 `jti` 追踪,从而让重放失败、让登出真正生效。 +- **守卫**:`get_current_user`、`get_current_active_superuser` 与 `require_scopes(...)` 都是普通的依赖。 +- **存储**:SQLite 开箱即用;设置 `DATABASE_URL` 并运行 `alembic upgrade head` 即可使用 PostgreSQL。 +- **部署之前**:固定 `SECRET_KEY`,并将 `DATABASE_URL` 指向真正的数据库。 + +## 接下来去哪里 + +- [SQLModel 持久化](sqlmodel.md) —— 同样的领域布局,侧重于异步数据访问、迁移与通用 CRUD。 +- [领域驱动项目](domain-starter.md) —— 本模板所基于的布局,不含认证机制。 +- [应该选择哪个 starter?](../user-guide/choosing-a-starter.md) —— 本模板与其他 starter 的对比。 diff --git a/docs/zh/tutorial/llm-agent.md b/docs/zh/tutorial/llm-agent.md new file mode 100644 index 0000000..a5c0ec4 --- /dev/null +++ b/docs/zh/tutorial/llm-agent.md @@ -0,0 +1,198 @@ +# 使用 `fastapi-llm-agent` 构建流式 LLM 智能体 + +构建一个聊天后端:它能在令牌生成时将其流式输出,并且能真正*做*事情 —— 调用工具、读取工具结果并继续推进。本教程会走完 `fastapi-llm-agent` 模板:SSE 事件协议、工具调用循环及其迭代上限、隐藏在接口之后的对话记忆,以及一个从不访问网络的测试套件。 + +## 您将学到的内容 + +- 使用 `fastkit startdemo fastapi-llm-agent` 生成项目 +- 客户端必须处理的 SSE 事件类型,以及每种事件的含义 +- 工具调用循环的一轮是如何逐步进行的 +- 添加您自己的工具 +- 将进程内对话存储替换为真正的存储 +- 为什么测试是确定性的,并且运行时不产生费用 + +## 前置条件 + +- Python 3.12+ +- 已安装 FastAPI-fastkit(`pip install fastapi-fastkit`) +- 一个 Anthropic API 密钥 —— 仅在与真实模型对话时需要。生成的测试套件无需密钥即可运行。 +- 熟悉 `async`/`await` 以及异步生成器 + +## 第 1 步:生成并配置 + +```console +$ fastkit startdemo fastapi-llm-agent +Enter the project name: support-agent +Enter the author name: Developer Kim +Enter the author email: developer@example.com +Enter the project description: Streaming support assistant +Select package manager (pip, uv, pdm, poetry) [uv]: uv +Do you want to proceed with project creation? [y/N]: y +``` + +首次运行前,请将密钥写入 `.env`: + +```bash +ANTHROPIC_API_KEY=sk-ant-... +LLM_MODEL=claude-sonnet-5 +LLM_MAX_TOKENS=4096 +MAX_TOOL_ITERATIONS=5 +``` + +密钥由 pydantic-settings 读取,并在构造客户端时传入。它从不写入源代码,也从不打包进 Docker 镜像。 + +```console +$ cd support-agent +$ bash scripts/run-server.sh +$ bash scripts/chat.sh "What is 12 * 34?" +``` + +!!! tip "使用网关代替 Anthropic API" + 设置 `LLM_BASE_URL`(如果网关使用 Bearer 认证,还需设置 `LLM_AUTH_TOKEN`),使其指向 LiteLLM、内部代理或本地服务器。传输格式仍然是 Anthropic Messages API;为其他提供商做转换是网关的职责,而不是应用的职责。 + +## 第 2 步:生成的目录树 + +``` +support-agent/ +├── pyproject.toml # PEP 621 metadata + [tool.fastapi-fastkit] +├── requirements.txt +├── Dockerfile +├── .env +├── scripts/ +│ ├── chat.sh # send one message to a running server +│ ├── format.sh lint.sh run-server.sh test.sh +├── src/ +│ └── app/ +│ ├── main.py # FastAPI app + lifespan +│ ├── core/config.py # pydantic-settings (API key lives here) +│ ├── llm/ +│ │ ├── client.py # AsyncAnthropic factory +│ │ ├── tools.py # tool schemas + handlers +│ │ └── agent.py # streaming agent + tool loop +│ ├── memory/ +│ │ ├── base.py # ConversationStore interface +│ │ └── in_memory.py # process-local implementation +│ ├── schemas/chat.py # request bodies + SSE event envelope +│ └── api/ +│ ├── router.py # aggregates health + chat +│ ├── deps.py # store / agent dependencies +│ ├── health.py # GET /health +│ └── chat.py # POST /chat (SSE), conversation history +└── tests/ + ├── conftest.py # scripted fake Anthropic client + ├── test_health.py test_chat.py test_tools.py test_memory.py +``` + +有三条边界值得一开始就注意:**客户端**由工厂创建(因此测试可以替换它),**记忆**是一个带有简单实现的接口(因此生产环境可以替换它),**工具**是一个模式列表加一个处理器字典(因此添加工具只需修改一个文件)。 + +## 第 3 步:端点 + +| 方法 | 端点 | 说明 | +|--------|------------------------------|-------------------------| +| GET | `/api/v1/health` | 存活探针 | +| POST | `/api/v1/chat` | 以 SSE 流式返回回复 | +| GET | `/api/v1/conversations/{id}` | 读取已存储的历史 | +| DELETE | `/api/v1/conversations/{id}` | 删除一段对话 | + +`POST /api/v1/chat` 接收 `{"message": "...", "conversation_id": "..."}`。省略 `conversation_id` 即可开始新的对话,并从最后的 `done` 事件中读取分配的 id。 + +## 第 4 步:流协议 + +每条 SSE 消息在 `event:` 字段中携带其类型,在 `data:` 中携带一个 JSON 封装。客户端必须处理其中五种事件以及错误: + +| 事件 | 字段 | 含义 | +|---------------|---------------------------------|--------------------------------| +| `token` | `text` | 助手回复的一个片段 | +| `tool_use` | `tool_name`, `tool_input` | Claude 请求调用工具 | +| `tool_result` | `tool_name`, `tool_output` | 工具已执行 | +| `usage` | `input_tokens`, `output_tokens` | 每轮的令牌消耗 | +| `done` | `conversation_id` | 本轮结束 | +| `error` | `detail` | 流式传输中途出错 | + +`tool_use` 和 `tool_result` 并不是装饰 —— 正是它们让 UI 能在工具运行时显示 "searching…",而不是一段无法解释的停顿。 + +每一轮的令牌用量也会写入 `app.agent` 日志记录器,因此无需在客户端埋点即可在服务器日志中看到消耗情况。 + +## 第 5 步:循环的一轮 + +`src/app/llm/agent.py` 中的 `ChatAgent.stream` 就是整个引擎。一轮的过程如下: + +1. 从存储中加载对话历史。 +2. 携带历史、新的用户消息和工具模式发起一个流式请求。 +3. 每收到一个文本片段,就立即以 `token` 事件产出。 +4. 流结束后读取最终消息;产出 `usage` 事件并记录令牌数量。 +5. **如果 `stop_reason != "tool_use"`,**说明助手已经完成:持久化本轮,产出 `done`,然后返回。 +6. **否则,**对每个 `tool_use` 块:产出 `tool_use`,运行处理器,产出 `tool_result`,并收集输出。 +7. 将所有工具结果作为一条 `user` 消息发回 —— 这是 Messages API 的要求 —— 然后从第 2 步开始循环。 + +循环受 `MAX_TOOL_ITERATIONS`(默认 5)限制。如果 Claude 在超过上限后仍持续请求工具,本轮会被持久化、记录一条警告并产出 `error` 事件,而不是让服务器空转: + +```python +yield AgentEvent( + type="error", + detail=( + "The assistant exceeded the tool-call limit of " + f"{self._max_tool_iterations} iterations." + ), +) +``` + +在智能体循环中,这样的上限并不是可有可无的。没有它,一个误解了工具结果的模型可能会无限期地向您计费。 + +## 第 6 步:自带的工具 + +| 工具 | 用途 | +|----------------|----------------------------------------------------------| +| `calculator` | 算术运算,解析为 AST 并进行白名单检查 —— 不使用 `eval` | +| `current_time` | 某个 IANA 时区的当前时间 | + +`calculator` 值得再看一眼。它使用 `ast` 解析表达式,并且只遍历白名单中的节点类型,因此来自模型的工具参数无法执行任意 Python 代码。任何接受模型生成输入的工具都需要同样的纪律:**模型输出是不可信的输入。** + +### 添加工具 + +所有内容都在 `src/app/llm/tools.py` 中: + +1. 向 `TOOL_SCHEMAS` 追加一个模式 —— `name`、`description` 以及一个 JSON Schema 格式的 `input_schema`。 +2. 在 `TOOL_HANDLERS` 中以相同名称注册处理器。 + +有两件事需要做对: + +- **描述是 Claude 关于何时使用该工具所得到的全部指令。**它是提示词工程,而不是文档 —— 含糊的描述会让工具要么从不被调用,要么被不停地调用。 +- **处理器应返回字符串,并把自身的失败转换为可读的消息,**而不是抛出异常。循环会把这段文本直接反馈给模型,因此 "no city named X was found" 能让模型自行恢复,而异常则会终止本轮。 + +## 第 7 步:对话记忆 + +`ConversationStore`(`src/app/memory/base.py`)是接口;自带的 `in_memory.py` 用一个字典实现了它。这对单个进程来说没问题,对其他任何情况来说都是错误的 —— 重启会忘记一切,两个 worker 对历史的认知也会不一致。 + +要让它可用于实际环境:针对 Redis 或数据库实现同一接口,并在 `src/app/api/deps.py` 的 `get_store()` 中返回它。智能体和路由都无需改动,因为它们都从不引用具体的类。 + +## 第 8 步:测试 + +```console +$ bash scripts/test.sh # or: pytest +``` + +`tests/conftest.py` 通过一个模仿 `AsyncAnthropic.messages.stream` 的假客户端回放预先编排好的对话轮次。因此测试套件是确定性的、可离线运行、不产生任何费用,同时仍然覆盖了重要的路径:流式路径和完整的工具循环,包括调用了工具、随后模型给出回答的一轮。 + +在扩展智能体时请保持这一模式。针对真实模型测试 LLM 集成,会得到一个缓慢、不稳定且昂贵的测试套件,而且仍然无法断言模型会说什么。针对编排好的客户端测试*循环*,断言的恰恰是您所编写的东西。 + +## 第 9 步:扩展点 + +- **持久化历史** —— 针对 Redis 或数据库实现 `ConversationStore`(第 7 步)。 +- **RAG** —— 模板刻意没有包含检索功能。最自然的接入点是 `ChatAgent.stream`:在第一次请求前获取上下文并将其添加到系统提示词开头,或者把您的索引作为另一个工具暴露出来,让模型决定何时搜索。 +- **认证与限流** —— 按出厂状态,`POST /chat` 没有认证。在公开暴露之前,请为路由器加上依赖;[JWT 认证教程](auth-jwt.md) 提供了可以复用的守卫。 + +## 回顾 + +- **生成**:`fastkit startdemo fastapi-llm-agent`,设置 `ANTHROPIC_API_KEY`,`bash scripts/run-server.sh`。 +- **流式传输**:SSE,包含 `token` / `tool_use` / `tool_result` / `usage` / `done` / `error` 事件。 +- **循环**:流式输出 → 工具调用 → 结果作为一条用户消息发回 → 重复,受 `MAX_TOOL_ITERATIONS` 限制。 +- **工具**:`TOOL_SCHEMAS` 中的一个模式加上 `TOOL_HANDLERS` 中的一个处理器;将工具输入视为不可信。 +- **记忆**:`ConversationStore` 接口,字典实现,在 `get_store()` 中替换。 +- **测试**:编排好的假客户端,因此测试套件可离线运行且具有确定性。 + +## 接下来去哪里 + +- [MCP 集成](mcp-integration.md) —— 如果您需要的是 MCP 服务器而不是聊天智能体。 +- [JWT 认证](auth-jwt.md) —— 在聊天端点之前加上账户体系。 +- [应该选择哪个 starter?](../user-guide/choosing-a-starter.md) diff --git a/docs/zh/tutorial/mcp-integration.md b/docs/zh/tutorial/mcp-integration.md index e26df37..ae86121 100644 --- a/docs/zh/tutorial/mcp-integration.md +++ b/docs/zh/tutorial/mcp-integration.md @@ -68,8 +68,8 @@ Deploying FastAPI project using 'fastapi-mcp' template │ Dependency 1 │ fastapi │ │ Dependency 2 │ uvicorn │ │ Dependency 3 │ pydantic │ -│ Dependency 4 │ python-jose │ -│ Dependency 5 │ passlib │ +│ Dependency 4 │ PyJWT │ +│ Dependency 5 │ pwdlib[argon2] │ │ Dependency 6 │ python-multipart│ │ Dependency 7 │ mcp │ └──────────────┴────────────────┘ @@ -131,21 +131,21 @@ ai-integrated-api/ ```python from datetime import datetime, timedelta from typing import Optional, Dict, Any -from jose import JWTError, jwt -from passlib.context import CryptContext +import jwt +from pwdlib import PasswordHash from src.core.config import settings -# 密码哈希处理 -pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") +# 密码哈希处理 —— argon2id,新应用推荐的默认方案。 +password_hash = PasswordHash.recommended() def verify_password(plain_password: str, hashed_password: str) -> bool: """校验密码""" - return pwd_context.verify(plain_password, hashed_password) + return password_hash.verify(plain_password, hashed_password) def get_password_hash(password: str) -> str: """生成密码哈希""" - return pwd_context.hash(password) + return password_hash.hash(password) def create_access_token(data: Dict[str, Any], expires_delta: Optional[timedelta] = None) -> str: """生成访问令牌""" @@ -189,7 +189,7 @@ def decode_token(token: str) -> Optional[Dict[str, Any]]: algorithms=[settings.ALGORITHM] ) return payload - except JWTError: + except jwt.PyJWTError: return None def verify_token(token: str, token_type: str = "access") -> Optional[str]: @@ -452,8 +452,7 @@ user_db = UserDatabase() from typing import Optional, List from fastapi import Depends, HTTPException, status, Security from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials, APIKeyHeader -from jose import JWTError - +import jwt from src.auth.jwt_handler import decode_token, token_manager from src.auth.models import User, UserInDB, Permission, user_db @@ -486,7 +485,7 @@ async def get_current_user( if user_id is None: raise credentials_exception - except JWTError: + except jwt.PyJWTError: raise credentials_exception user = user_db.get_user_by_id(user_id) diff --git a/docs/zh/tutorial/sqlmodel.md b/docs/zh/tutorial/sqlmodel.md new file mode 100644 index 0000000..fee6a7c --- /dev/null +++ b/docs/zh/tutorial/sqlmodel.md @@ -0,0 +1,260 @@ +# 使用 `fastapi-sqlmodel` 实现异步持久化 + +构建一个由数据库支撑、且每个概念只定义**一次**的 API。SQLModel 类同时是 SQLAlchemy 数据表和 pydantic 模式,因此添加一列并不意味着要修改四个文件。本教程会走完 `fastapi-sqlmodel` 模板:模型层级、通用 CRUD 基类、分页响应、异步 Alembic 迁移,以及如何添加您自己的领域。 + +## 您将学到的内容 + +- 使用 `fastkit startdemo fastapi-sqlmodel` 生成项目 +- `ItemBase` → `Item` / `ItemCreate` / `ItemUpdate` / `ItemPublic` 如何消除模式重复 +- `CRUDBase` 免费提供了什么,以及在哪里扩展它 +- `Page[T]` 列表封装,以及其限制在哪里被强制执行 +- 运行异步 Alembic 迁移,并从 SQLite 切换到 PostgreSQL + +## 前置条件 + +- Python 3.12+ +- 已安装 FastAPI-fastkit(`pip install fastapi-fastkit`) +- 熟悉 `async`/`await` 以及基本的 SQL 概念 + +如果您想要的是同步的、基于 Compose 的 PostgreSQL 技术栈,请参阅使用 `fastapi-psql-orm` 的 [数据库集成](database-integration.md)。 + +## 第 1 步:生成并运行 + +```console +$ fastkit startdemo fastapi-sqlmodel +Enter the project name: catalog-api +Enter the author name: Developer Kim +Enter the author email: developer@example.com +Enter the project description: Product catalog service +Select package manager (pip, uv, pdm, poetry) [uv]: uv +Do you want to proceed with project creation? [y/N]: y +``` + +```console +$ cd catalog-api +$ bash scripts/migrate.sh # alembic upgrade head +$ bash scripts/run-server.sh # or: uvicorn src.app.main:app --reload +``` + +默认的 `DATABASE_URL` 是 `sqlite+aiosqlite:///./app.db`,因此无需安装数据库服务器。文档位于 。 + +## 第 2 步:生成的目录树 + +``` +catalog-api/ +├── pyproject.toml # PEP 621 metadata + [tool.fastapi-fastkit] +├── requirements.txt +├── alembic.ini +├── Dockerfile +├── .env +├── scripts/ +│ ├── format.sh lint.sh migrate.sh run-server.sh test.sh +├── src/ +│ └── app/ +│ ├── main.py # FastAPI app + lifespan +│ ├── core/config.py # pydantic-settings +│ ├── db/ +│ │ ├── session.py # async engine, get_session dependency +│ │ └── base.py # metadata aggregation for Alembic +│ ├── crud/ +│ │ ├── base.py # generic CRUDBase +│ │ └── pagination.py # Page[T] envelope +│ ├── alembic/ +│ │ ├── env.py # async migration environment +│ │ └── versions/0001_create_items_table.py +│ ├── api/ +│ │ ├── router.py # aggregates health + domain routers +│ │ └── health.py # GET /health +│ └── domains/items/ # the example domain +│ ├── models.py # Item table + Create/Update/Public schemas +│ ├── crud.py # ItemCRUD(CRUDBase) +│ └── router.py # FastAPI router +└── tests/ + ├── conftest.py test_health.py test_items.py test_crud_base.py +``` + +请注意领域文件夹中*没有*的东西:没有独立于 `models.py` 的 `schemas.py`,也没有每个领域各自的 repository 样板代码。两者分别被 SQLModel 和 `CRUDBase` 吸收了。 + +## 第 3 步:每个概念一个模型定义 + +打开 `src/app/domains/items/models.py`。所有内容都派生自同一个基类: + +```python +class ItemBase(SQLModel): + """Fields shared by the table and every request/response schema.""" + name: str = Field(min_length=1, max_length=128, index=True) + description: str | None = Field(default=None, max_length=512) + price: float = Field(ge=0) + in_stock: bool = True + + +class Item(ItemBase, table=True): # the actual table + __tablename__ = "items" + id: int | None = Field(default=None, primary_key=True) + created_at: datetime = Field(default_factory=_utcnow) + + +class ItemCreate(ItemBase): ... # POST body — no server-assigned fields + + +class ItemUpdate(SQLModel): # PATCH body — every field optional + name: str | None = Field(default=None, min_length=1, max_length=128) + ... + + +class ItemPublic(ItemBase): # response — base + server-assigned + id: int + created_at: datetime +``` + +四个类,四种不同的职责,字段只存在于一处: + +| 类 | 角色 | 为什么要单独定义 | +|--------------|-------------------------------|-------------------| +| `ItemBase` | 共享的字段定义 | 唯一的事实来源 | +| `Item` | 数据库表 | 添加 `id`、`created_at` | +| `ItemCreate` | `POST` 的请求体 | 不得接受 `id` | +| `ItemUpdate` | `PATCH` 的请求体 | 所有字段均为可选 | +| `ItemPublic` | 响应体 | 暴露由服务器分配的字段 | + +**收益:**添加一个 `sku` 列只需在 `ItemBase` 中加一行。数据表获得该列,创建请求体接受它,响应返回它 —— 无需任何其他修改。`ItemUpdate` 则刻意保持显式:部分更新的语义是一项决策,而不是一种推导。 + +## 第 4 步:无需重写的 CRUD + +`src/app/crud/base.py` 是一个以表模型及其两个输入模式为参数的泛型类: + +```python +class CRUDBase(Generic[ModelType, CreateSchemaType, UpdateSchemaType]): + async def get(self, session, obj_id) -> ModelType | None: ... + async def list(self, session, *, offset=0, limit=20) -> Sequence[ModelType]: ... + async def count(self, session) -> int: ... + async def create(self, session, obj_in) -> ModelType: ... + async def update(self, session, db_obj, obj_in) -> ModelType: ... + async def delete(self, session, db_obj) -> None: ... +``` + +有一个细节值得牢记 —— `update` 使用了 `exclude_unset=True`: + +```python +changes: dict[str, Any] = obj_in.model_dump(exclude_unset=True) +``` + +正是这一点让 `PATCH` 真正成为部分更新。客户端没有发送的字段不会出现在字典中,因此永远不会被写入;如果没有 `exclude_unset`,`PATCH` 会悄无声息地用模式默认值覆盖未触及的列。 + +领域只需继承基类,并只添加它特有的部分: + +```python +class ItemCRUD(CRUDBase[Item, ItemCreate, ItemUpdate]): + """CRUD for Item, extended with a name search.""" + + async def search_by_name(self, session, term, *, limit=20): + statement = select(Item).where(Item.name.like(f"%{term}%")).limit(limit) + result = await session.exec(statement) + return result.all() + + +item_crud = ItemCRUD(Item) +``` + +## 第 5 步:分页列表 + +列表端点返回的是 `Page[T]` 封装,而不是裸数组: + +```json +{"items": [...], "total": 42, "offset": 0, "limit": 20} +``` + +限制值的截断发生在路由中,而不是客户端: + +```python +limit: int = Query(default=settings.DEFAULT_PAGE_LIMIT, ge=1) +... +limit = min(limit, settings.MAX_PAGE_LIMIT) +``` + +因此 `?limit=100000` 无法把一次请求变成全表扫描。`Page` 还提供一个由 `offset`、`len(items)` 和 `total` 推导出的 `has_next` 属性,这通常正是前端真正需要的。 + +items 领域自带的端点: + +| 方法 | 端点 | 说明 | +|--------|---------------------------|--------------------------------| +| GET | `/api/v1/health` | 存活探针 | +| GET | `/api/v1/items` | 列出项目(`offset`、`limit`) | +| GET | `/api/v1/items/{item_id}` | 读取单个项目 | +| POST | `/api/v1/items` | 创建项目(`201`) | +| PATCH | `/api/v1/items/{item_id}` | 部分更新项目 | +| DELETE | `/api/v1/items/{item_id}` | 删除项目(`204`) | + +动手试试: + +```console +$ curl -X POST http://127.0.0.1:8000/api/v1/items \ + -H 'Content-Type: application/json' \ + -d '{"name": "Keyboard", "price": 89.0}' + +$ curl 'http://127.0.0.1:8000/api/v1/items?offset=0&limit=5' +``` + +## 第 6 步:从头到尾都是异步 + +`src/app/db/session.py` 为每个进程构建一个 `AsyncEngine`,并分发请求作用域的会话: + +```python +async def get_session() -> AsyncGenerator[AsyncSession, None]: + async with async_session_factory() as session: + yield session +``` + +路由依赖它(`session: AsyncSession = Depends(get_session)`),`CRUDBase` 将它作为第一个参数,每个数据库调用都会被 await。请求路径中没有任何代码会因 I/O 阻塞事件循环。 + +`create_db_and_tables()` 也位于这里,并由应用的 `lifespan` 调用。它是为默认的 SQLite 和测试提供的便利功能 —— 一旦由 Alembic 管理数据库模式,请设置 `CREATE_TABLES_ON_STARTUP=false`。 + +## 第 7 步:添加领域 + +1. 参照 `items` 创建 `src/app/domains//`(`models.py`、`crud.py`、`router.py`)。 +2. **在 `src/app/db/base.py` 中导入表模型。**这一步很容易忘记,调试起来也很令人困惑:Alembic 自动生成和 `create_db_and_tables` 只能看到已被导入的模型,因此缺少导入时表现为一个空迁移,而不是报错。 +3. 在 `src/app/api/router.py` 中注册路由器。`# fastkit:imports` 和 `# fastkit:routes` 锚点标记了 `fastkit addroute` 插入代码的位置 —— 请把您的代码放在同样的地方。 +4. 生成迁移:`alembic revision --autogenerate -m "add "`。 +5. 参照 `tests/test_items.py` 添加 `tests/test_.py`。 + +## 第 8 步:迁移与 PostgreSQL + +`src/app/alembic/env.py` 下的 Alembic 环境是异步的,因此它与应用使用同一套驱动栈运行。日常使用: + +```console +$ bash scripts/migrate.sh # upgrade head +$ alembic revision --autogenerate -m "add sku to items" # after a model change +``` + +切换数据库只需修改 URL 并添加一个驱动: + +```console +$ uv pip install "asyncpg>=0.31.0" +$ export DATABASE_URL="postgresql+asyncpg://user:password@localhost:5432/app" +$ bash scripts/migrate.sh +``` + +模型、CRUD 和路由都无需改动 —— 这正是把驱动隐藏在 `create_engine` 之后的意义所在。 + +## 第 9 步:测试 + +```console +$ bash scripts/test.sh # or: pytest +``` + +每个测试都针对一个全新的内存 SQLite 数据库运行:无需迁移,无需 fixture 文件,测试之间也无需清理。`test_crud_base.py` 直接测试通用基类 —— 在扩展它之前值得一读,因为它记录了 `exclude_unset` 的行为以及您自己的领域所继承的 count/offset 契约。 + +## 回顾 + +- **生成**:`fastkit startdemo fastapi-sqlmodel` → `bash scripts/migrate.sh` → `bash scripts/run-server.sh`。 +- **模型**:`ItemBase` 持有字段;数据表与输入/输出模式都派生自它,因此新列只需声明一次。 +- **CRUD**:`CRUDBase` 提供 get / list / count / create / update / delete;领域通过继承它来添加自己的查询。 +- **列表**:`Page[T]` 封装,并在服务器端截断限制值。 +- **模式**:异步 Alembic;记得在 `src/app/db/base.py` 中导入新模型。 +- **数据库**:默认使用 SQLite;修改 `DATABASE_URL` 并安装 `asyncpg` 即可使用 PostgreSQL。 + +## 接下来去哪里 + +- [JWT 认证](auth-jwt.md) —— 在这个数据层之前加上账户与受保护的路由。 +- [领域驱动项目](domain-starter.md) —— 两个模板共享的布局,不含数据库。 +- [应该选择哪个 starter?](../user-guide/choosing-a-starter.md) diff --git a/docs/zh/user-guide/choosing-a-starter.md b/docs/zh/user-guide/choosing-a-starter.md index 29b3a46..1c586f4 100644 --- a/docs/zh/user-guide/choosing-a-starter.md +++ b/docs/zh/user-guide/choosing-a-starter.md @@ -17,6 +17,9 @@ FastAPI-fastkit 提供了多种项目起步方式。本页是一份**面向新 | 想要尽可能小的骨架 | `fastkit init --interactive`(预设:**`minimal`**) | | 编写快速原型 / 单文件脚本 | `fastkit init --interactive`(预设:**`single-module`**) | | 需要真实的数据库(PostgreSQL + SQLAlchemy + Alembic) | `fastkit startdemo fastapi-psql-orm` | +| 想要带迁移和通用 CRUD 的异步持久化 | `fastkit startdemo fastapi-sqlmodel` | +| 第一天就需要用户账户、登录和受保护的路由 | `fastkit startdemo fastapi-auth-jwt` | +| 要构建支持流式输出和工具调用的 LLM 聊天后端 | `fastkit startdemo fastapi-llm-agent` | | 想要面向中型 API 的生产风格领域布局 | `fastkit init --interactive`(预设:**`domain-starter`**) | ## `startdemo` 与 `init --interactive` —— 有什么区别? @@ -112,11 +115,36 @@ $ fastkit init --interactive | `fastapi-empty` | `minimal` | 裸骨架;与 `minimal` 落地后的形态相同。 | | `fastapi-single-module` | `single-module` | 单文件示例。 | | `fastapi-domain-starter` | `domain-starter` | 推荐的现代默认选项;自带 items 示例领域。 | -| `fastapi-async-crud` | `classic-layered` | `fastapi-default` 的异步版本。 | +| `fastapi-auth-jwt` | `domain-starter` | 您的 API 需要账户体系。访问/刷新令牌、argon2id 哈希、角色与作用域、Alembic 迁移。 | +| `fastapi-sqlmodel` | `domain-starter` | 异步持久化优先:SQLModel + 异步 SQLAlchemy、迁移、通用 CRUD 基类与分页列表。 | +| `fastapi-llm-agent` | `domain-starter` | 带有真正工具调用循环和离线测试的流式 Claude 聊天后端。 | | `fastapi-custom-response` | `classic-layered` | 演示自定义响应包装 / 格式化。 | -| `fastapi-dockerized` | `classic-layered` | 在默认布局之上加入生产级 Dockerfile。 | -| `fastapi-psql-orm` | (无直接对应预设) | PostgreSQL + SQLAlchemy + Alembic。需要真实数据库时选它。 | +| `fastapi-psql-orm` | (无直接对应预设) | PostgreSQL + SQLAlchemy + Alembic,同步方式。当您想要一个围绕真实数据库的 Compose 技术栈时选它。 | | `fastapi-mcp` | (无直接对应预设) | Model Context Protocol 集成。 | +| `fastapi-async-crud` | `classic-layered` | ⚠️ **已弃用。**`fastapi-default` 的异步版本;建议改用 `fastapi-sqlmodel`。 | +| `fastapi-dockerized` | `classic-layered` | ⚠️ **已弃用。**在默认布局之上加入 Dockerfile;当前每个模板都已附带 Dockerfile。 | + +### 三个较新的 starter + +这三个模板在 v1.4.0 中加入,都遵循 `domain-starter` 的结构(`src/app/` 加上按概念划分的文件夹),因此可以与您在该预设中已经熟悉的内容组合使用。 + +**`fastapi-auth-jwt` —— 经得起生产考验的认证。** +访问令牌有效期很短;刷新令牌每次使用都会轮换,并通过数据库中的 `jti` 进行追踪,因此被重放的令牌会被拒绝,而不是被悄悄接受。密码通过 `pwdlib` 使用 argon2id 哈希。登出是真正生效的:可以吊销一个会话,也可以吊销某个用户的所有会话。守卫以依赖的形式提供 —— `get_current_user`、`get_current_active_superuser`,以及一个 `require_scopes(...)` 工厂。它在 SQLite 上运行,无需外部服务,设置 `DATABASE_URL` 即可迁移到 PostgreSQL。完整演练:[JWT 认证](../tutorial/auth-jwt.md)。 + +**`fastapi-sqlmodel` —— 每个概念一个模型定义。** +SQLModel 类既是 SQLAlchemy 数据表又是 pydantic 模式,因此新列只需声明一次,所有请求/响应模式都会继承它。从头到尾都是异步,包括异步 Alembic 迁移;`CRUDBase` 提供 create / read / list / count / update / delete,因此每个领域只需编写自己特有的部分;列表端点则包装在分页封装中。完整演练:[SQLModel 持久化](../tutorial/sqlmodel.md)。 + +**`fastapi-llm-agent` —— 一个聊天后端,而不是 `POST /completion` 的包装。** +令牌通过 SSE 流式发送给客户端,工具调用循环会一直运行,直到模型完成或触发迭代上限。对话历史位于 `ConversationStore` 接口之后,因此从内置的字典迁移到 Redis 只需提供一个实现。测试套件用一个编排好的假客户端替换 Anthropic 客户端 —— 确定性、可离线运行、无需 API 密钥。完整演练:[LLM 智能体](../tutorial/llm-agent.md)。 + +### 关于已弃用的模板 + +`fastapi-async-crud` 与 `fastapi-dockerized` 仍然随包提供,也仍然能生成可运行的项目 —— 使用它们不会出现任何问题。之所以标记为已弃用,是因为选择它们的理由已经不复存在: + +- `fastapi-dockerized` 的存在是为了添加 Dockerfile。当前每个模板都已附带 Dockerfile,而交互式部署选项也会为任意预设生成 Docker 文件。 +- `fastapi-async-crud` 演示的是基于 mock 存储的异步端点。`fastapi-sqlmodel` 做的是同样的事情,但面向真实的异步数据库,并且带有迁移。 + +由这两个模板生成的现有项目无需任何处理。 `fastkit list-templates` 会显示当前可用模板及其一行说明。 @@ -141,5 +169,6 @@ $ fastkit init --interactive - [快速上手](quick-start.md) —— 真正动手创建您的第一个项目。 - [创建项目](creating-projects.md) —— 更深入地了解 CLI 参数。 +- [JWT 认证教程](../tutorial/auth-jwt.md)、[SQLModel 持久化教程](../tutorial/sqlmodel.md) 与 [LLM 智能体教程](../tutorial/llm-agent.md) —— v1.4.0 新增的三个 starter 的端到端演练。 - [领域驱动项目教程](../tutorial/domain-starter.md) —— 如果您选择了 `domain-starter`,这是从生成的目录树、内置 `items` 示例,到如何添加下一个领域的完整演练。 - [架构预设矩阵](../reference/preset-feature-matrix.md) —— 每个预设 / 功能的完整契约。 diff --git a/docs/zh/user-guide/cli-reference.md b/docs/zh/user-guide/cli-reference.md index dc6e8f4..5b1f563 100644 --- a/docs/zh/user-guide/cli-reference.md +++ b/docs/zh/user-guide/cli-reference.md @@ -60,9 +60,127 @@ $ fastkit init [OPTIONS] | 选项 | 描述 | 默认值 | |--------|-------------|---------| +| `--interactive` | 引导式设置:先选择架构预设,再选择功能 | off | +| `--config ` | 根据已保存的配置文件创建项目,不进行任何提示 | - | +| `--save-config ` | 与 `--interactive` 一起使用,将回答完成的配置写入此路径 | - | +| `--project-name` | 项目名称(省略时会提示输入) | - | +| `--author` | 作者名称(省略时会提示输入) | - | +| `--author-email` | 作者邮箱(省略时会提示输入) | - | +| `--description` | 项目描述(省略时会提示输入) | - | | `--package-manager` | 要使用的包管理器(pip、uv、pdm、poetry) | uv | +| `--dry-run` | 显示将要创建的文件和包,但不写入任何内容 | off | +| `--no-venv` | 跳过虚拟环境创建(隐含 `--no-install`) | off | +| `--no-install` | 跳过依赖安装 | off | +| `--yes` / `-y` | 在原地部署项目时,覆盖已有文件之前不再询问确认 | off | | `--help` | 显示命令帮助 | - | +#### 配置文件(`--config` / `--save-config`) + +交互式会话的回答是一个普通的映射,而这个映射可以通过文件传递。这使得项目创建变成了可以提交、审查和重放的东西。 + +```console +# 记录一次交互式会话 +$ fastkit init --interactive --save-config fastkit.config.json + +# 之后重放它,完全不进行提示 +$ fastkit init --config fastkit.config.json +``` + +如果您没有传入 `--save-config`,一次走到最后的交互式运行会询问是否保存所选内容以及保存位置。该提示只会在有人坐在终端前时出现 —— 通过管道或在 CI 中运行时保持静默,改为通过该选项显式开启。 + +**支持的格式**根据文件扩展名选择: + +| 扩展名 | 可用性 | +|---|---| +| `.json` | 始终可用(标准库) | +| `.toml` | 始终可用(标准库) | +| `.yaml` / `.yml` | 仅在安装了 PyYAML 时可用 —— fastkit 不会将其作为运行时依赖添加 | + +加载的文件顶层必须是一个映射,且至少包含 `project_name`、`author` 和 `author_email`。随后它会按照交互式提示所用的相同规则进行校验(项目名称格式、邮箱格式、`all_dependencies` 必须是列表),并在写入任何内容之前报告所有问题。只列出功能选择而没有 `all_dependencies` 的文件,会通过交互式构建器展开,因此手写的配置与向导会话会解析出相同的包集合。 + +一个最小示例: + +```json +{ + "project_name": "orders-api", + "author": "Developer Kim", + "author_email": "developer@example.com", + "description": "Domain-oriented orders service", + "architecture_preset": "domain-starter", + "package_manager": "uv" +} +``` + +#### 配置文件模式 + +每个配置文件 —— 无论是手写的还是由 `--save-config` 生成的 —— 在到达生成器之前都会经过同一个规范化器(`backend/project_builder/config_schema.py` 中的 `normalize_project_config`)。未知的键、已知维度上的未知选项,或类型错误的值都会被拒绝,并给出出错的名称以及实际允许的值列表,而不是被悄悄忽略。 + +| 键 | 形式 | 说明 | +|---|---|---| +| `project_name`, `author`, `author_email` | 字符串 | 必填 | +| `description` | 字符串 | 必填 | +| `architecture_preset`(别名 `preset`) | `"minimal"` \| `"single-module"` \| `"classic-layered"` \| `"domain-starter"` | 如果两种写法同时出现,它们必须一致 | +| `package_manager` | `"pip"` \| `"uv"` \| `"pdm"` \| `"poetry"` | | +| `database` | 字符串,或 `{"type": }` | `PostgreSQL`、`MySQL`、`MongoDB`、`Redis`、`SQLite`、`None` | +| `authentication`, `async_tasks`, `testing`, `caching`, `monitoring`, `migrations`, `logging` | 字符串,或 `{"type": }` | 单选维度 —— 各维度的选项请参阅上文的[功能目录](#interactive-builder-feature-catalog-interactive) | +| `utilities`, `tooling` | 字符串列表 | 多选维度 —— 选项见同一目录 | +| `deployment` | 字符串列表 | `Docker`、`docker-compose` 的子集;选择 `docker-compose` 会自动带上 `Docker` | +| `custom_packages` | 字符串列表 | 按原样安装的额外包 | + +每个维度都既接受单纯的选项(`"authentication": "JWT"`),也接受 `database` 和项目自身 `[tool.fastapi-fastkit]` 元数据所使用的 `{"type": ...}` 映射形式(`"authentication": {"type": "JWT"}`)—— 两者都会被规范化为相同的标准形式,而 `--save-config` 总是以标准的纯字符串形式写回。 + +一个覆盖了大多数维度的配置: + +```json +{ + "project_name": "orders-api", + "author": "Developer Kim", + "author_email": "developer@example.com", + "description": "Domain-oriented orders service", + "architecture_preset": "domain-starter", + "package_manager": "uv", + "database": "PostgreSQL", + "authentication": "JWT", + "async_tasks": "Celery", + "caching": "Redis", + "migrations": "Alembic", + "tooling": ["ruff", "makefile"], + "deployment": ["Docker"] +} +``` + +同样的配置写成 TOML: + +```toml +project_name = "orders-api" +author = "Developer Kim" +author_email = "developer@example.com" +description = "Domain-oriented orders service" +architecture_preset = "domain-starter" +package_manager = "uv" +database = "PostgreSQL" +authentication = "JWT" +async_tasks = "Celery" +caching = "Redis" +migrations = "Alembic" +tooling = ["ruff", "makefile"] +deployment = ["Docker"] +``` + +#### 预览与跳过步骤 + +`--dry-run` 会打印将要创建的目录树以及将要安装的包 —— 连同将用来安装它们的包管理器 —— 然后在不触碰磁盘的情况下退出: + +```console +$ fastkit init --config fastkit.config.json --dry-run +``` + +`--no-install` 会在虚拟环境创建后停止;`--no-venv` 则连环境也一并跳过(因此也不会安装)。两者在容器、CI 以及任何由 fastkit 之外的工具管理环境的场景中都很有用。 + +#### 原地部署与覆盖确认(`--yes` / `-y`) { #in-place-deployment-and-overwrite-confirmation-yes-y } + +当项目以原地方式部署 —— 部署到当前工作区而不是新的项目文件夹 —— 时,fastkit 会在替换任何已存在的文件之前询问 "Overwrite these files?"。`--yes`(或其简写 `-y`)会跳过该确认并直接覆盖。它不影响另一个 "Do you want to proceed with project creation?" 提示,该提示仍会显示。在非交互环境中(stdin 不是 TTY,例如 CI 或通过管道运行),即使没有 `--yes`,覆盖确认也会被自动跳过。 + #### 交互式提示 `init` 命令会依次提示您输入: @@ -96,6 +214,27 @@ $ fastkit init [OPTIONS] - `redis` —— 内存数据存储 - `celery` —— 分布式任务队列 +#### 交互式构建器功能目录(`--interactive`) { #interactive-builder-feature-catalog-interactive } + +`fastkit init --interactive` 使用的是另一套基于维度的目录,而不是上面的 minimal/standard/full 栈(事实来源:`core/settings.py` 中的 `FastkitConfig.PACKAGE_CATALOG`,以及决定每个选项生成什么的 `DynamicConfigGenerator.build_artifacts()`)。每个维度默认都是 `None`(跳过);`utilities` 与 `tooling` 为多选。 + +现在每个选择都会生成真正的代码,而不仅仅是安装包 —— `minimal` / `single-module`(重新生成 `main.py`)与 `classic-layered` / `domain-starter`(保留 `main.py`,并给出手动接入警告)之间有何不同,请参阅[架构预设 / 功能矩阵](../reference/preset-feature-matrix.md)。 + +| 维度 | 选项 | 安装的包 | 生成的文件 | +|---|---|---|---| +| `database` | PostgreSQL, MySQL, MongoDB, Redis, SQLite, None | PostgreSQL: `asyncpg`, `sqlalchemy` · MySQL: `aiomysql`, `sqlalchemy` · MongoDB: `motor` · Redis: `redis[hiredis]` · SQLite: `sqlalchemy`, `aiosqlite` | 位于预设对应路径的数据库配置模块(例如 `src/config/database.py`) | +| `authentication` | JWT, OAuth2, FastAPI-Users, Session-based, None | JWT: `pyjwt[crypto]`, `pwdlib[argon2]` · OAuth2: `authlib`, `itsdangerous`, `httpx` · FastAPI-Users: `fastapi-users[sqlalchemy]`, `pyjwt[crypto]`, `pwdlib[argon2]` · Session-based: `itsdangerous` | 位于预设对应路径的认证配置模块;OAuth2 与 Session-based 还会添加 `main.py` 中间件配置 | +| `async_tasks` | Celery, Dramatiq, None | Celery: `celery[redis]`, `redis[hiredis]` · Dramatiq: `dramatiq[redis]`, `redis[hiredis]` | `/worker.py`(后台 worker)+ `/features/tasks.py`(任务路由) | +| `testing` | Basic, Coverage, Advanced, None | Basic: `pytest`, `pytest-asyncio`, `httpx` · Coverage: + `pytest-cov` · Advanced: + `faker`, `factory-boy` | `pytest.ini`;Advanced 还会添加 `tests/factories.py` 与 `tests/test_factories.py` | +| `caching` | Redis, None | Redis: `redis[hiredis]`, `fastapi-cache2`, `jinja2` | `/features/cache.py`(带缓存的端点) | +| `monitoring` | Loguru, OpenTelemetry, Prometheus, None | Loguru: `loguru` · OpenTelemetry: `opentelemetry-api`, `opentelemetry-sdk`, `opentelemetry-instrumentation-fastapi`, `opentelemetry-exporter-otlp-proto-http` · Prometheus: `prometheus-client`, `prometheus-fastapi-instrumentator` | 不生成独立文件 —— 直接接入 `main.py`(import / lifespan / setup) | +| `utilities`(多选) | CORS, Rate-Limiting, Pagination, WebSocket, None | CORS: 无(FastAPI 内置) · Rate-Limiting: `slowapi` · Pagination: `fastapi-pagination` · WebSocket: `websockets` | CORS 与 Rate-Limiting 直接接入 `main.py`;Pagination 添加 `/features/pagination.py`,WebSocket 添加 `/features/websocket.py` | +| `migrations` | Alembic, None | Alembic: `alembic` | `alembic.ini`、`alembic/env.py`、`alembic/script.py.mako`、基线修订 `alembic/versions/_initial.py`、`scripts/migrate.sh` —— 仅当数据库维度为 SQL 数据库时生成 | +| `tooling`(多选) | ruff, pre-commit, github-actions, devcontainer, makefile, None | ruff: `ruff` · pre-commit: `pre-commit` · github-actions / devcontainer / makefile: 无 | ruff 会把 `[tool.ruff]` 块合并进 `pyproject.toml`(`pip` 项目则写入 `ruff.toml`);pre-commit 写入 `.pre-commit-config.yaml`;github-actions 写入 `.github/workflows/test.yml`;devcontainer 写入 `.devcontainer/devcontainer.json`;makefile 写入 `Makefile` | +| `logging` | structured, None | 无(仅使用标准库 `logging` + `json`) | `/logging_config.py`(结构化 JSON 日志 + request-id 中间件),并接入 `main.py` | + +无论如何选择,每个生成的项目都会在 `main.py` 中获得 `/health` 与 `/ready` 端点,而 `--dry-run` 会在写入任何内容之前打印出这一组确切的文件。 + #### 示例
@@ -227,6 +366,26 @@ $ fastkit addroute users my-api | `PUT` | `/api/v1/users/{user_id}` | 更新用户 | | `DELETE` | `/api/v1/users/{user_id}` | 删除用户 | +#### 代码插入的位置 + +`addroute` 需要从项目中得知两件事:新的 import 放在哪里,以及在哪里注册路由器。模板和生成的入口文件都用锚点注释标出了这两处: + +```python +# src/app/api/router.py +from src.app.api import health + +# fastkit:imports + +api_router = APIRouter() +api_router.include_router(health.router) + +# fastkit:routes +``` + +编辑该文件时请保留这些注释 —— `addroute` 会直接插入到 `# fastkit:imports` 和 `# fastkit:routes` 的上方。丢失了这些锚点(或在锚点出现之前生成)的项目仍然可以工作:fastkit 会退回到基于 AST 的插入方式,自行找到 import 块和路由注册位置。锚点只是让结果变得可预测。 + +`addroute` 还会读取项目的 `[tool.fastapi-fastkit]` 块来定位入口文件,因此即使在仅凭磁盘扫描会判断错误的布局上也能正常工作。 + ### `startdemo` 基于预构建的模板创建 FastAPI 项目。 @@ -241,9 +400,19 @@ $ fastkit startdemo [OPTIONS] | 选项 | 描述 | 默认值 | |--------|-------------|---------| +| `--project-name` | 项目名称(省略时会提示输入) | - | +| `--author` | 作者名称(省略时会提示输入) | - | +| `--author-email` | 作者邮箱(省略时会提示输入) | - | +| `--description` | 项目描述(省略时会提示输入) | - | | `--package-manager` | 要使用的包管理器(pip、uv、pdm、poetry) | uv | +| `--dry-run` | 显示将要创建的文件和包,但不写入任何内容 | off | +| `--no-venv` | 跳过虚拟环境创建(隐含 `--no-install`) | off | +| `--no-install` | 跳过依赖安装 | off | +| `--yes` / `-y` | 在原地部署项目时,覆盖已有文件之前不再询问确认 | off | | `--help` | 显示命令帮助 | - | +与 `init` 一样,当目标项目目录已经存在时,`startdemo` 会拒绝运行(`Error: Project '{name}' already exists.`)。`--dry-run` 是例外:由于它从不写入磁盘,因此仍然可以指定一个已有同名目录的名称。关于部署到当前工作区时 `--yes` 的作用,请参阅上文的[原地部署与覆盖确认](#in-place-deployment-and-overwrite-confirmation-yes-y)。 + #### 交互式提示 `startdemo` 命令会依次提示您输入: @@ -259,11 +428,19 @@ $ fastkit startdemo [OPTIONS] | 模板 | 描述 | 特性 | |----------|-------------|----------| | `fastapi-default` | 简单的 FastAPI 项目 | 基础 CRUD、mock 数据 | -| `fastapi-async-crud` | 异步 item 管理 API | async/await、性能优化 | +| `fastapi-domain-starter` | 面向领域的 starter | 每个业务概念一个文件夹、`/health` | +| `fastapi-auth-jwt` | JWT 认证 | 访问/刷新令牌轮换、argon2id、角色与作用域、Alembic | +| `fastapi-sqlmodel` | 异步 SQLModel 持久化 | SQLModel + 异步 SQLAlchemy、Alembic、通用 CRUD、分页 | +| `fastapi-llm-agent` | 流式 Claude 智能体 | SSE 流式输出、工具调用循环、对话记忆 | | `fastapi-custom-response` | 自定义响应系统 | 自定义响应、分页 | -| `fastapi-dockerized` | 已 Docker 化的 FastAPI | Docker、适合生产部署 | | `fastapi-psql-orm` | 基于 PostgreSQL 的 FastAPI | PostgreSQL、SQLAlchemy、Alembic | +| `fastapi-mcp` | Model Context Protocol 服务器 | MCP 集成 | +| `fastapi-single-module` | 单文件沙盒 | 单个模块,没有包边界 | | `fastapi-empty` | 最小化的 FastAPI 项目 | 极简骨架 | +| `fastapi-async-crud` | *(已弃用)* 异步 item 管理 API | 已被 `fastapi-sqlmodel` 取代 | +| `fastapi-dockerized` | *(已弃用)* 已 Docker 化的 FastAPI | Docker 工具链现已随较新的模板一同提供 | + +已弃用的模板仍然可以生成可运行的项目;只是不再推荐作为起点。请参阅[应该选择哪个 starter?](choosing-a-starter.md)。 #### 示例 @@ -331,9 +508,11 @@ INFO: Uvicorn running on http://127.0.0.1:8000 #### 前置条件 - 必须在 FastAPI 项目目录中运行 -- 项目必须含有 `src/main.py`,并定义了 FastAPI 应用 +- 项目必须暴露一个 FastAPI 应用 —— 如果存在 `[tool.fastapi-fastkit].app_module`,就从中解析,否则通过扫描 `main.py` 来发现 - 建议提前激活虚拟环境 +如果记录的入口和扫描都找不到模块,`runserver` 会报告找不到 `main.py` 并停止。 + ### `list-templates` 列出所有可用的 FastAPI 项目模板。 @@ -372,6 +551,35 @@ $ fastkit list-templates
+## 项目元数据(`[tool.fastapi-fastkit]`) + +fastkit 生成的每个项目都会在自己的 `pyproject.toml` 中记录它是如何生成的: + +```toml +[tool.fastapi-fastkit] +managed = true +version = "1.4.0" +template = "fastapi-domain-starter" +preset = "domain-starter" +package_manager = "uv" +app_module = "src.app.main:app" +features = ["database:PostgreSQL", "authentication:JWT"] +``` + +| 键 | 含义 | +|---|---| +| `managed` | 始终为 `true`;将项目标记为由 fastkit 管理 | +| `version` | 生成该项目的 fastkit 版本 | +| `template` | `startdemo` 模板,或预设的基础模板 | +| `preset` | `init --interactive` 的架构预设(其他情况下省略) | +| `package_manager` | 用于构建环境的包管理器 | +| `app_module` | uvicorn 的 `module:attr` 入口 | +| `features` | 交互式选择的扁平 `":"` 列表 | + +这个块是项目入口的唯一事实来源:`fastkit runserver` 会先从中读取 `app_module`,然后才退回到扫描目录树;`fastkit addroute` 用它定位应用;生成的 Dockerfile 也在 `CMD` 中使用同一个值。 + +该块在生成的最后阶段写入 —— 包管理器在解析依赖时会重写 `pyproject.toml`,因此必须在那之后再写入 —— 之后的重新生成会替换它,而不是再追加一份。手动编辑 `app_module` 是告知 fastkit 您自行重组了布局的受支持方式。 + ## 环境变量 FastAPI-fastkit 会读取以下环境变量: @@ -381,6 +589,14 @@ FastAPI-fastkit 会读取以下环境变量: | `FASTKIT_CONFIG_DIR` | 配置目录 | `~/.fastkit` | | `FASTKIT_TEMPLATES_DIR` | 自定义模板目录 | 内置模板 | | `FASTKIT_LOG_LEVEL` | 日志级别 | `INFO` | +| `FASTKIT_SUBPROCESS_TIMEOUT` | 覆盖所有包管理器子进程的超时时间,单位为秒 | 按操作区分(30 / 120 / 900) | + +每次包管理器调用都有各自的时间上限,因此挂起的子进程永远不会阻塞 CLI:可用性检查为 30 秒,创建虚拟环境为 120 秒,安装依赖为 900 秒。`FASTKIT_SUBPROCESS_TIMEOUT` 会用一个值替换这三者 —— 在网络较慢时,或想在 CI 中快速失败时很有用。不是正整数的值会被忽略。 + +```console +$ export FASTKIT_SUBPROCESS_TIMEOUT=1800 +$ fastkit startdemo fastapi-sqlmodel +``` ### 示例 diff --git a/docs/zh/user-guide/creating-projects.md b/docs/zh/user-guide/creating-projects.md index f0a4b18..fcbbddd 100644 --- a/docs/zh/user-guide/creating-projects.md +++ b/docs/zh/user-guide/creating-projects.md @@ -462,7 +462,7 @@ settings = Settings()
```console -$ pip install requests httpx python-jose +$ pip install requests httpx pyjwt $ pip freeze > requirements.txt ```