diff --git a/CHANGELOG.md b/CHANGELOG.md index 72d1d18..3647e73 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,37 @@ # Changelog +## v1.5.0 (2026-10-14) + +### Features + +- `fastkit addroute --layout classic-layer|domain` (contributed by @vastimofeev, #83). Without the option, the layout is resolved from `[tool.fastapi-fastkit]`: `preset = "domain-starter"` selects `domain`, other presets keep `classic-layer`; when no preset is recorded (`startdemo`, older `init` projects), a `template = "fastapi-domain-starter"` entry selects `domain`. The resolved layout is shown in the confirmation table. +- `domain` layout generates `domains//` with `models.py`, `schemas.py`, `repository.py`, `service.py`, `router.py`, and a re-exporting `__init__.py`; class names are derived from the route (`health_checks` → `HealthChecksService`). The example is a typed CRUD API backed by a thread-safe in-memory repository (process-local, lost on restart) with a `reset_store()` hook, and is registered in the shared API router with `prefix="/"` and `tags=[""]`. +- Re-running `addroute` is idempotent for both layouts: existing files are kept, existing `include_router` registrations (positional or `router=` keyword form) are not duplicated, and custom prefix/options on an existing registration are preserved with an info message. An unparsable router module falls back to text insertion, and import aliases (`_router`, `_2`, …) let same-named modules from different layouts coexist. + +### Templates + +- `fastapi-domain-starter`: the shipped `items` domain now follows the same convention (`APIRouter()` in the domain, `prefix="/items", tags=["items"]` on `include_router`; endpoints unchanged), and `tests/conftest.py` resets the in-memory store of every domain that exposes `service.reset_store()` before and after each test, so generated domains stay isolated (#83). + +### Security + +- `PyJWT` floor raised to 2.15.1 in `fastapi-auth-jwt` and `fastapi-mcp` (PYSEC-2026-4140 ~ 4152); `fastapi-mcp` pins `urllib3==2.8.0` (PYSEC-2026-4175 ~ 4177). +- Workspace security floors `urllib3>=2.8.0` (GHSA-gh4c-6fx4-qh6g) and `anyio>=4.14.2` (GHSA-82r6-8w77-94w6, GHSA-5p39-cfhj-2xmp) added to `pyproject.toml`; `uv.lock` (anyio 4.14.2, urllib3 2.8.0), `pdm.lock` (anyio 4.15.1, urllib3 2.8.0), and `requirements-docs.txt` (urllib3 2.8.0) relocked (#80, #82). +- Dev extra floor `virtualenv>=21.7.13` (transitive via `pre-commit`) closes GHSA-p58f-9548-mpm2 (bash/fish activation scripts execute commands embedded in paths), GHSA-x78j-v8h9-3j2q (command injection via `--prompt` in `activate.bat`), GHSA-94p9-xgh2-xp45 (seed wheels not integrity-checked) and GHSA-9h9j-4vrj-gf7g (`pyvenv.cfg` prompt injection); `requirements.txt` pinned to 21.7.16 and `uv.lock` / `pdm.lock` relocked (virtualenv 21.7.9 → 21.7.16, no other packages moved). + +### Documentation + +- Docs landing pages (root and every locale) now load the logo from `img/fastkit_general_logo.png` instead of the repo-only `.github/` path (#81). +- English: `adding-routes`, `cli-reference`, and the domain-starter tutorial document `--layout` and the domain layout (#83); the MCP tutorial's `dependencies.py` snippet gains the missing `import jwt`; `development-setup` says "see above" instead of "see below". +- All six translated locales (ko/ja/zh/es/fr/de) brought to parity with English: the `auth-jwt`, `sqlmodel`, and `llm-agent` tutorials translated, drifted pages (`index`, `development-setup`, `template-creation-guide`, `preset-feature-matrix`, `template-quality-assurance`, `cli-reference`, `creating-projects`, `choosing-a-starter`, `mcp-integration`) re-synced, and the translation-status page updated (#84). The three pages changed by #83 (domain-starter tutorial, `adding-routes`, `cli-reference`) are re-synced in every locale in this release. + +### Maintenance + +- `addroute` generation moved into `backend/route_generators.py` (`ClassicLayeredRouteGenerator`, `DomainRouteGenerator`, `resolve_route_layout`) and `backend/route_wiring.py` (`register_router`, `router_alias`); `add_new_route()` gained an optional `layout` argument. Suite size: **1047 tests passing**. + +### Breaking Changes + +- `addroute` in a `domain-starter` project (or a project generated from `fastapi-domain-starter` without a recorded preset) now generates the domain layout instead of `api/routes` / `crud` / `schemas` modules; pass `--layout classic-layer` to keep the old output. + ## v1.4.1 (2026-09-10) ### Security diff --git a/docs/de/reference/translation-status.md b/docs/de/reference/translation-status.md index 51f0f72..a7ddaee 100644 --- a/docs/de/reference/translation-status.md +++ b/docs/de/reference/translation-status.md @@ -19,14 +19,14 @@ Die unten stehenden Zahlen zählen die Markdown-Seiten im Verzeichnisbaum jeder | Sprache | Status | Markdown-Seiten | Anmerkungen | |---|---|---:|---| | 🇬🇧 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-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: +| 🇰🇷 Koreanisch (`ko`) | ✅ Vollständig | 29 / 29 | Phasen 1–3 abgeschlossen. Am 2026-10-14 mit den englischen Quellen von v1.5.0 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-14 mit den englischen Quellen von v1.5.0 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-14 mit den englischen Quellen von v1.5.0 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-14 mit den englischen Quellen von v1.5.0 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-14 mit den englischen Quellen von v1.5.0 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-14 mit den englischen Quellen von v1.5.0 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-10-14, nach dem Release von v1.5.0. 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 diff --git a/docs/de/tutorial/domain-starter.md b/docs/de/tutorial/domain-starter.md index c2ef074..c06775c 100644 --- a/docs/de/tutorial/domain-starter.md +++ b/docs/de/tutorial/domain-starter.md @@ -7,7 +7,7 @@ Bauen Sie einen mittelgroßen FastAPI-Dienst mit dem empfohlenen modernen Layout - Ein Projekt mit `fastkit startdemo fastapi-domain-starter` generieren - Die Rolle von `core`, `db`, `domains` und `tests` im Layout - Wie eine Domäne in router → service → repository → schemas → models aufgeteilt wird -- Den Vertrag zum Hinzufügen einer neuen Domäne (items-Ordner kopieren, Router registrieren) +- Eine neue Domäne mit `fastkit addroute` hinzufügen und ihren Speicher in Tests isolieren - Wie der mitgelieferte `/health`-Endpunkt und das `/api/v1/items`-CRUD in die App eingebunden werden ## Voraussetzungen @@ -76,7 +76,7 @@ orders-api/ │ ├── schemas.py # ItemCreate, ItemRead (pydantic) │ ├── repository.py # ItemRepository over InMemoryStore │ ├── service.py # ItemService + ItemNotFoundError -│ └── router.py # APIRouter(prefix="/items") +│ └── router.py # APIRouter endpoints └── tests/ ├── __init__.py ├── conftest.py # TestClient fixture, store reset @@ -138,7 +138,7 @@ Zwei Teile: # src/app/api/router.py api_router = APIRouter() api_router.include_router(health.router) -api_router.include_router(items_router.router) +api_router.include_router(items_router.router, prefix="/items", tags=["items"]) ``` ```python @@ -166,7 +166,7 @@ Die **Abhängigkeitsrichtung** ist `router → service → repository → store` Spiegelt das Laufzeit-Layout — pro Oberfläche, deren Verhalten festgepinnt werden soll, ein Testmodul. Der Starter liefert: -- `conftest.py` — autouse-Fixture, die den Items-Store zwischen Tests zurücksetzt, plus eine `client`-Fixture, die `TestClient(app)` umhüllt. +- `conftest.py` — autouse-Fixture, die zwischen Tests den Items-Store zurücksetzt und für generierte Domänen `service.reset_store()` aufruft, plus eine `client`-Fixture, die `TestClient(app)` umhüllt. - `test_health.py` — prüft, dass `GET /api/v1/health` 200 + `{"status": "ok"}` liefert. - `test_items.py` — vollständige CRUD-Abdeckung der Items-Endpunkte, inklusive eines 404 für unbekannte IDs und eines 422 für ungültige Payloads. @@ -245,7 +245,7 @@ Der Router ist das einzige Stück, das HTTP kennt. Beachten Sie, dass er den Ser ```python # src/app/domains/items/router.py -router = APIRouter(prefix="/items", tags=["items"]) +router = APIRouter() def get_item_service() -> ItemService: return ItemService() @@ -258,6 +258,13 @@ def get_item(item_id: int, service: ItemService = Depends(get_item_service)) -> raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(exc)) ``` +Der gemeinsame API-Router setzt das Präfix und die Tags der Domäne: + +```python +# src/app/api/router.py +api_router.include_router(items_router.router, prefix="/items", tags=["items"]) +``` + Der vollständige Router stellt bereit: | Methode | Pfad | Wirkung | @@ -286,99 +293,71 @@ $ curl http://127.0.0.1:8000/api/v1/items/999 ## Schritt 5: Ihre nächste Domäne hinzufügen -Der Starter ist so gestaltet, dass das **Hinzufügen einer Domäne eine Kopier-und-Umbenenn-Operation** ist. Angenommen, Sie möchten eine `users`-Domäne neben `items`: - -### 1. Den `items/`-Ordner kopieren +Fügen Sie vom Wurzelverzeichnis des generierten Projekts aus eine `users`-Domäne hinzu: ```console -$ cp -r src/app/domains/items src/app/domains/users +$ fastkit addroute users ``` -### 2. Entity, Schemas und Klassennamen pro Datei umschreiben - -```python -# src/app/domains/users/models.py -from dataclasses import dataclass +Der Befehl liest `[tool.fastapi-fastkit].preset` oder leitet es aus `template` ab, wenn `preset` fehlt. Projekte, die mit `startdemo`, interaktivem `init` oder `init --config` erstellt wurden, wählen daher für domain-starter das Domänen-Layout. Das gewählte Layout wird vor der Bestätigung angezeigt. Sie können es auch explizit auswählen: -@dataclass -class User: - id: int - email: str - is_active: bool = True -``` - -```python -# src/app/domains/users/schemas.py -from pydantic import BaseModel, ConfigDict, Field - -class UserCreate(BaseModel): - # Ein einfaches ``str`` sorgt dafür, dass das Beispiel direkt funktioniert. - # Wenn Sie stattdessen Pydantics eingebaute E-Mail-Validierung nutzen möchten, - # installieren Sie die optionale Abhängigkeit - # (``pip install 'pydantic[email]'`` — dadurch wird ``email-validator`` mitinstalliert) - # und ersetzen ``str`` durch ``EmailStr``. - email: str = Field(min_length=3, max_length=320) - is_active: bool = True - -class UserRead(BaseModel): - id: int - email: str - is_active: bool - model_config = ConfigDict(from_attributes=True) +```console +$ fastkit addroute users . --layout=domain ``` -Benennen Sie `Item → User`, `ItemNotFoundError → UserNotFoundError`, `ItemRepository → UserRepository`, `ItemService → UserService` in `models.py`, `schemas.py`, `repository.py`, `service.py` und `router.py` um. Vergessen Sie nicht `prefix="/items"` → `prefix="/users"` und `tags=["items"]` → `tags=["users"]` im Router. - -Das Repository kann dasselbe `InMemoryStore`-gestützte Muster behalten — es ist generisch über den Entity-Typ: +`--layout=classic-layer` generiert die klassische Struktur aus `api/routes`, `crud` und `schemas`. Das ist auch der Standard für `classic-layered`, `minimal`, `single-module` sowie für Projekte ohne Preset- oder Template-Metadaten. Ein unbekanntes Preset fällt mit einer Warnung auf `classic-layer` zurück. Die Option ändert das Preset des Projekts nicht. -```python -# src/app/domains/users/repository.py -_store: InMemoryStore[User] = InMemoryStore() +### Generierte Domäne -class UserRepository: - def __init__(self, store: Optional[InMemoryStore[User]] = None) -> None: - self._store = store if store is not None else _store - # ... same shape as ItemRepository ... +```text +src/app/domains/users/ +├── __init__.py +├── models.py # Users with id and name +├── schemas.py # UsersCreate and UsersRead +├── repository.py # UsersRepository: typed, in-memory CRUD +├── service.py # UsersService and UsersNotFoundError +└── router.py # endpoints and get_users_service dependency ``` -### 3. Die `__init__.py` der Domäne aktualisieren +Die neue Domäne verwendet PascalCase-Klassennamen, die aus dem Routennamen abgeleitet werden; aus `users` muss also kein Singular abgeleitet werden. Passen Sie diese Dateien an, um die Felder und das Verhalten einzuführen, die Ihr Geschäftskonzept benötigt. -Die items-Domäne re-exportiert ihre Module, sodass Aufrufer `from src.app.domains.items import service` schreiben können. Spiegeln Sie das für users: +Der Router wird automatisch in `src/app/api/router.py` mit `prefix="/users"` und `tags=["users"]` registriert. Domänen-Router verwenden `APIRouter()` ohne diese Einstellungen. Das bestehende Anwendungspräfix bleibt erhalten, sodass diese Endpunkte bereitstehen: -```python -# src/app/domains/users/__init__.py -from src.app.domains.users import ( # noqa: F401 - models, - repository, - router, - schemas, - service, -) -``` +| Methode | Endpunkt | Ergebnis | +|--------|----------|--------| +| GET | `/api/v1/users` | Entities auflisten, 200 | +| GET | `/api/v1/users/{entity_id}` | Entity lesen, 200 oder 404 | +| POST | `/api/v1/users` | Entity anlegen, 201 | +| PUT | `/api/v1/users/{entity_id}` | Entity ersetzen, 200 oder 404 | +| DELETE | `/api/v1/users/{entity_id}` | Entity löschen, leeres 204 oder 404 | -### 4. Den Router im Aggregator registrieren +POST und PUT akzeptieren `{"name": "Alice"}`; `name` muss 1–120 Zeichen lang sein. Das Repository vergibt IDs beginnend bei 1. PUT behält diese ID bei. Ein PATCH-Endpunkt wird nicht generiert. -Dies ist die **einzige Datei außerhalb von `domains/users/`, die Sie anfassen müssen**: +Das neue Repository ist eigenständig; es nutzt nicht den `db.memory`-Store der mitgelieferten items-Domäne. Jede explizit erzeugte Repository-Instanz hat einen unabhängigen Speicher, während die Standardinstanz zwischen Anfragen innerhalb dieser Domäne und dieses Prozesses geteilt wird. CRUD-Operationen sind durch einen Lock geschützt. Daten gehen beim Neustart verloren und werden nicht zwischen Worker-Prozessen geteilt; ersetzen Sie das Repository, wenn Sie dauerhaften Speicher benötigen. -```python -# src/app/api/router.py -from src.app.api import health -from src.app.domains.items import router as items_router -from src.app.domains.users import router as users_router # ← add +### Isolierte Tests hinzufügen -api_router = APIRouter() -api_router.include_router(health.router) -api_router.include_router(items_router.router) -api_router.include_router(users_router.router) # ← add -``` +Die items-spezifische Reset-Fixture setzt die neue Domäne nicht zurück. Überschreiben Sie deren Service-Dependency für jeden Test mit einem frischen Repository: -Nach einem Server-Neustart sehen Sie `/api/v1/users` in `/docs` eingebunden. +```python +from src.app.domains.users.repository import UsersRepository +from src.app.domains.users.router import get_users_service +from src.app.domains.users.service import UsersService -### 5. Tests hinzufügen +def test_create_user(client): + from src.app.main import app -Spiegeln Sie `tests/test_items.py` als `tests/test_users.py` — dieselbe client-getriebene Form, einfach gegen die neuen Endpunkte. Die autouse-Fixture zur Store-Reset in `conftest.py` hält jeden Test bereits isoliert. + service = UsersService(repository=UsersRepository()) + app.dependency_overrides[get_users_service] = lambda: service + try: + response = client.post("/api/v1/users", json={"name": "Alice"}) + assert response.status_code == 201 + assert response.json() == {"id": 1, "name": "Alice"} + finally: + app.dependency_overrides.pop(get_users_service, None) +``` -Wenn Sie eine zweite Domäne hinzufügen, die ebenfalls `InMemoryStore` nutzt, erweitern Sie die Fixture, um auch deren Store zurückzusetzen, oder behalten Sie eine Fixture pro Domäne. +Ein erneutes Ausführen von `addroute` erhält bestehende Domänendateien, erstellt fehlende Dateien und vermeidet doppelte Imports und Registrierungen. Gleichnamige klassische Module können über Import-Aliase nebeneinander bestehen. Generierte Pakete re-exportieren ihre Module, und Repositories stellen `reset()` bereit, um Daten zu löschen und die IDs neu zu starten. Die autouse-Fixture des Starters ruft für generierte Domänen `service.reset_store()` auf. Ergänzen Sie diesen Aufruf in älteren Projekten in der bestehenden Reset-Fixture oder verwenden Sie isolierte Repositories über Dependency-Overrides. Prüfen Sie HTTP-Pfade und -Methoden selbst, wenn Sie Router kombinieren; der Generator erkennt keine überlappenden Endpunkte. ## Schritt 6: Wie es weitergeht @@ -390,4 +369,4 @@ Wenn Sie eine zweite Domäne hinzufügen, die ebenfalls `InMemoryStore` nutzt, e - **Generierung**: `fastkit startdemo fastapi-domain-starter` → `bash scripts/run-server.sh` → Doku unter `/docs`. - **Layout**: `core/` für Konfiguration, `db/` für Persistenz-Abstraktionen, `domains//` für Geschäfts-Scheiben, `api/router.py` als zentrale Aggregationsstelle, `tests/` als Spiegel der Laufzeitmodule. -- **Domäne hinzufügen**: `items/` kopieren, Entity / Schemas / Klassen umbenennen, Re-Exports in `__init__.py` aktualisieren, Router in `src/app/api/router.py` registrieren, Testmodul ergänzen. Keine Änderung an `main.py`. +- **Domäne hinzufügen**: `fastkit addroute users` ausführen, das generierte `Users*`-CRUD-Gerüst anpassen und Tests mit isolierten Repositories ergänzen. Der Router wird automatisch registriert. diff --git a/docs/de/user-guide/adding-routes.md b/docs/de/user-guide/adding-routes.md index c8c2c7c..db81a19 100644 --- a/docs/de/user-guide/adding-routes.md +++ b/docs/de/user-guide/adding-routes.md @@ -2,8 +2,64 @@ Lernen Sie, wie Sie neue API-Routen zu Ihrem bestehenden FastAPI-Projekt hinzufügen. +## Das Routen-Layout wählen + +`addroute` akzeptiert `--layout=classic-layer` oder `--layout=domain`. Ohne Angabe liest der Befehl `[tool.fastapi-fastkit].preset`: `domain-starter` wählt `domain`; `classic-layered`, `minimal` und `single-module` wählen `classic-layer`. Fehlt `preset`, wird das Preset aus `template` abgeleitet, sodass auch Projekte, die mit `startdemo` oder interaktivem `init` erstellt wurden, für `fastapi-domain-starter` `domain` wählen. Ohne einen dieser Werte wird `classic-layer` gewählt. Ein unbekanntes Preset fällt mit einer Warnung auf `classic-layer` zurück. Eine explizite Option überschreibt diese Auswahl, ohne die Metadaten des Projekts zu ändern. + +```console +$ cd my-api +$ fastkit addroute users . --layout=domain +$ fastkit addroute products . --layout=classic-layer +``` + +Das gewählte Layout wird vor der Bestätigung angezeigt. Die Dateien werden unter dem tatsächlichen Paket der Anwendung (`src/` oder `src/app/`) abgelegt, wobei der hinterlegte Einstiegspunkt verwendet wird, sofern vorhanden. + +## Ein Domänen-CRUD generieren + +Für ein `domain-starter`-Projekt genügt: + +```console +$ fastkit addroute users +``` + +Dies erstellt die folgenden Dateien und registriert den Domänen-Router automatisch: + +```text +src/app/domains/users/ +├── __init__.py +├── models.py # Users(id: int, name: str) +├── schemas.py # UsersCreate for POST/PUT, UsersRead for responses +├── repository.py # typed CRUD operations and in-memory storage +├── service.py # business logic and UsersNotFoundError +└── router.py # HTTP endpoints and overridable service dependency +``` + +Die Klassen heißen innerhalb jeder Domäne `Users`, `UsersCreate`, `UsersRead`, `UsersRepository` und `UsersService`; Klassennamen verwenden PascalCase (`health_checks` wird zu `HealthChecks`), ohne Singularbildung. Das Repository vergibt IDs beginnend bei 1. POST und PUT erfordern einen `name` mit 1 bis 120 Zeichen; PUT behält die bestehende ID bei. + +| Methode | Domänenpfad | Ergebnis | +|--------|-------------|--------| +| GET | `/users` | Entities auflisten, 200 | +| GET | `/users/{entity_id}` | Entity lesen, 200 oder 404 | +| POST | `/users` | Entity anlegen, 201 | +| PUT | `/users/{entity_id}` | Entity ersetzen, 200 oder 404 | +| DELETE | `/users/{entity_id}` | Entity löschen, leeres 204 oder 404 | + +Präfix und Tags der Domäne werden wie beim klassischen Layout im gemeinsamen API-Router beim Aufruf von `include_router` gesetzt. + +Das bestehende API-Präfix der Anwendung bleibt wirksam (zum Beispiel `/api/v1/users`). Domänenrouten enthalten kein PATCH. + +Das generierte Repository ist eigenständig und schützt seine CRUD-Operationen mit einem Lock. Seine Standardinstanz wird zwischen Anfragen **innerhalb dieser Domäne und dieses Prozesses** geteilt. Daten gehen beim Neustart verloren und werden nicht zwischen Worker-Prozessen geteilt. Passen Sie das Repository für dauerhafte Persistenz an; es hängt weder von `db.memory` des Starters noch von einem ORM oder einer Datenbankkonfiguration ab. + +Für isolierte Tests injizieren Sie ein frisches `UsersRepository` in `UsersService` und überschreiben anschließend die FastAPI-Dependency `get_users_service` des Routers. `UsersRepository.reset()` leert dieses Repository und startet die IDs wieder bei 1. `service.reset_store()` setzt das zwischen Anfragen geteilte Standard-Repository zurück. Explizit erzeugte Repository-Instanzen haben einen unabhängigen Speicher. + +## Den Befehl erneut ausführen + +Bestehende Moduldateien bleiben erhalten; fehlende Dateien werden erstellt. Imports und Router-Registrierungen werden nicht dupliziert. Gleichnamige Module in verschiedenen Layouts können nebeneinander bestehen; Import-Aliase vermeiden Namenskollisionen in Python. Konflikte bei HTTP-Methode/-Pfad prüft der Generator nicht. `--layout` migriert keine bestehenden Module. + ## Grundlegendes Hinzufügen einer Route +Die folgenden Beispiele beschreiben das Layout `classic-layer`. Übergeben Sie `--layout=classic-layer` explizit, wenn Sie ein domain-starter-Projekt verwenden. + ### Den Befehl `addroute` verwenden Der Befehl `addroute` von FastAPI-fastkit erleichtert das Hinzufügen neuer Routen: diff --git a/docs/de/user-guide/cli-reference.md b/docs/de/user-guide/cli-reference.md index 0edf75d..d2a7a8b 100644 --- a/docs/de/user-guide/cli-reference.md +++ b/docs/de/user-guide/cli-reference.md @@ -311,6 +311,7 @@ $ fastkit addroute ROUTE_NAME [PROJECT_DIR] [OPTIONS] | Option | Beschreibung | Standard | |--------|-------------|---------| +| `--layout [classic-layer\|domain]` | Struktur des generierten Routenmoduls; überschreibt die Preset-Metadaten | Automatisch aus dem Preset | | `--help` | Hilfe zum Befehl anzeigen | - | #### Beispiele @@ -346,25 +347,40 @@ $ fastkit addroute users my-api #### Generierte Dateien -Erstellt diese Dateien im Projekt: +Mit `--layout=classic-layer` werden diese Dateien unter dem App-Paket erstellt: - `src/api/routes/users.py` — Routen-Handler - `src/crud/users.py` — CRUD-Operationen - `src/schemas/users.py` — Pydantic-Schemas -Aktualisiert außerdem `src/api/api.py`, um den neuen Router einzubinden. +Mit `--layout=domain` wird `domains/users/` mit `__init__.py`, `models.py`, `schemas.py`, `repository.py`, `service.py` und `router.py` erstellt. Die Entity hat `id: int` und `name: str` und kommt mit einem funktionierenden In-Memory-Repository. + +Beide Layouts aktualisieren den bestehenden API-Router und verbinden ihn bei Bedarf mit der Anwendung. Die Pfade folgen dem tatsächlichen App-Paket, einschließlich `src/app/`. + +Ohne die Option wählt `[tool.fastapi-fastkit].preset = "domain-starter"` das Layout `domain`. Fehlt `preset`, wird das Preset aus `template` abgeleitet; auch `fastapi-domain-starter` wählt `domain`. Andere Presets und Projekte ohne einen dieser Metadatenwerte wählen `classic-layer`. Ein unbekanntes Preset fällt mit einer Warnung auf `classic-layer` zurück. Eine explizite Auswahl ändert das Preset des Projekts nicht. + +```console +$ fastkit addroute users . --layout=domain +$ fastkit addroute products . --layout=classic-layer +``` + +Der Befehl zeigt das gewählte Layout vor der Bestätigung an. Ein erneutes Ausführen erhält bestehende Dateien, stellt fehlende Dateien wieder her und vermeidet doppelte Router-Registrierungen. Gleichnamige Module verschiedener Layouts können über Import-Aliase nebeneinander bestehen. Der Generator prüft keine Konflikte bei HTTP-Methode/-Pfad. Eine Migration erfolgt manuell. #### Generierte Endpunkte -Erstellt vollständige CRUD-Endpunkte: +Das Domänen-Layout erstellt diese Endpunkte (dargestellt mit `/api/v1` als bestehendem Präfix der App): | Methode | Endpunkt | Beschreibung | |--------|----------|-------------| -| `GET` | `/api/v1/users/` | Alle Nutzer abrufen | -| `POST` | `/api/v1/users/` | Neuen Nutzer anlegen | -| `GET` | `/api/v1/users/{user_id}` | Bestimmten Nutzer abrufen | -| `PUT` | `/api/v1/users/{user_id}` | Nutzer aktualisieren | -| `DELETE` | `/api/v1/users/{user_id}` | Nutzer löschen | +| `GET` | `/api/v1/users` | Entities auflisten, 200 | +| `POST` | `/api/v1/users` | Entity anlegen, 201 | +| `GET` | `/api/v1/users/{entity_id}` | Entity lesen, 200 oder 404 | +| `PUT` | `/api/v1/users/{entity_id}` | Entity ersetzen, 200 oder 404 | +| `DELETE` | `/api/v1/users/{entity_id}` | Entity löschen, leeres 204 oder 404 | + +POST und PUT akzeptieren einen `name` mit 1–120 Zeichen. IDs werden vom Repository vergeben, und PUT behält sie bei. Für Domänen wird kein PATCH-Endpunkt generiert. Der Speicher ist prozesslokal, geht beim Neustart verloren und ist zwischen Domänen unabhängig; ersetzen Sie das Repository für dauerhafte Persistenz. Tests können über den Service ein frisches Repository injizieren und `get_users_service` überschreiben. + +Das klassische Layout behält seine bestehenden GET/POST/PUT/PATCH/DELETE-Stubs unter `//` bei, die Sie selbst implementieren. #### Wo der Code eingefügt wird @@ -382,7 +398,7 @@ 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. +Lassen Sie diese Kommentare beim Bearbeiten der Datei an ihrem Platz — `addroute` fügt direkt unterhalb 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. diff --git a/docs/en/reference/translation-status.md b/docs/en/reference/translation-status.md index 555d488..4f31514 100644 --- a/docs/en/reference/translation-status.md +++ b/docs/en/reference/translation-status.md @@ -34,14 +34,14 @@ next section explains). | Locale | Status | Markdown pages | Notes | |---|---|---:|---| | 🇬🇧 English (`en`) | ✅ Source of truth | 29 / 29 | Authoritative. | -| 🇰🇷 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 +| 🇰🇷 Korean (`ko`) | ✅ Complete | 29 / 29 | Phases 1-3 complete. Re-synced with the v1.5.0 English sources on 2026-10-14: 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.5.0 English sources on 2026-10-14: 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.5.0 English sources on 2026-10-14: 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.5.0 English sources on 2026-10-14: 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.5.0 English sources on 2026-10-14: 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.5.0 English sources on 2026-10-14: 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-14, after the v1.5.0 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 diff --git a/docs/es/reference/translation-status.md b/docs/es/reference/translation-status.md index 7c3dfcc..0cec313 100644 --- a/docs/es/reference/translation-status.md +++ b/docs/es/reference/translation-status.md @@ -19,14 +19,14 @@ 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 | 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. | - -*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: +| 🇰🇷 Coreano (`ko`) | ✅ Completo | 29 / 29 | Phases 1-3 completadas. Resincronizado con las fuentes inglesas de v1.5.0 el 2026-10-14: 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.5.0 el 2026-10-14: 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.5.0 el 2026-10-14: 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.5.0 el 2026-10-14: 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.5.0 el 2026-10-14: 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.5.0 el 2026-10-14: 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. | + +*Instantánea verificada el 2026-10-14, tras la release v1.5.0. 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 diff --git a/docs/es/tutorial/domain-starter.md b/docs/es/tutorial/domain-starter.md index c7911bf..c88ac7f 100644 --- a/docs/es/tutorial/domain-starter.md +++ b/docs/es/tutorial/domain-starter.md @@ -7,7 +7,7 @@ Construye un servicio FastAPI de tamaño medio con la estructura moderna recomen - Generar un proyecto con `fastkit startdemo fastapi-domain-starter` - El papel de `core`, `db`, `domains` y `tests` dentro de la estructura - Cómo se divide un dominio en router → service → repository → schemas → models -- El contrato para añadir un dominio nuevo (copiar la carpeta items, registrar el router) +- Añadir un dominio nuevo con `fastkit addroute` y aislar su almacenamiento en los tests - Cómo se integran en la app el endpoint `/health` y el CRUD `/api/v1/items` ## Requisitos previos @@ -76,7 +76,7 @@ orders-api/ │ ├── schemas.py # ItemCreate, ItemRead (pydantic) │ ├── repository.py # ItemRepository sobre InMemoryStore │ ├── service.py # ItemService + ItemNotFoundError -│ └── router.py # APIRouter(prefix="/items") +│ └── router.py # endpoints de APIRouter └── tests/ ├── __init__.py ├── conftest.py # fixture TestClient, reinicio del almacén en memoria @@ -138,7 +138,7 @@ Dos piezas: # src/app/api/router.py api_router = APIRouter() api_router.include_router(health.router) -api_router.include_router(items_router.router) +api_router.include_router(items_router.router, prefix="/items", tags=["items"]) ``` ```python @@ -166,7 +166,7 @@ La **dirección de las dependencias** es `router → service → repository → Refleja la estructura de la aplicación en ejecución: un módulo de tests por cada superficie cuyo comportamiento conviene dejar cubierto. El starter trae: -- `conftest.py` — un fixture `autouse` que reinicia el almacén de items entre tests, además de un fixture `client` que envuelve `TestClient(app)`. +- `conftest.py` — un fixture `autouse` que, entre tests, reinicia el almacén de items y llama a `service.reset_store()` para los dominios generados, además de un fixture `client` que envuelve `TestClient(app)`. - `test_health.py` — verifica que `GET /api/v1/health` devuelve 200 y `{"status": "ok"}`. - `test_items.py` — cobertura completa de CRUD de los endpoints de items, incluyendo 404 para ids desconocidos y 422 para un payload inválido. @@ -245,7 +245,7 @@ El router es la única pieza que conoce HTTP. Fíjate en que recibe el service m ```python # src/app/domains/items/router.py -router = APIRouter(prefix="/items", tags=["items"]) +router = APIRouter() def get_item_service() -> ItemService: return ItemService() @@ -258,6 +258,13 @@ def get_item(item_id: int, service: ItemService = Depends(get_item_service)) -> raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(exc)) ``` +El router compartido de la API establece el prefijo y las etiquetas del dominio: + +```python +# src/app/api/router.py +api_router.include_router(items_router.router, prefix="/items", tags=["items"]) +``` + El router completo expone: | Método | Ruta | Qué hace | @@ -286,98 +293,71 @@ $ curl http://127.0.0.1:8000/api/v1/items/999 ## Paso 5: Añadir tu siguiente dominio -El starter está pensado para que **añadir un dominio sea tan simple como copiar y renombrar**. Imagina que quieres un dominio `users` junto a `items`: - -### 1. Copia la carpeta `items/` +Desde la raíz del proyecto generado, añade un dominio `users`: ```console -$ cp -r src/app/domains/items src/app/domains/users +$ fastkit addroute users ``` -### 2. Reescribe la entidad, los esquemas y los nombres de clase por archivo - -```python -# src/app/domains/users/models.py -from dataclasses import dataclass +El comando lee `[tool.fastapi-fastkit].preset`, o lo infiere a partir de `template` cuando falta `preset`. Por eso, los proyectos creados con `startdemo`, con `init` interactivo o con `init --config` eligen el layout de dominio para domain-starter. El layout seleccionado se muestra antes de la confirmación. También puedes seleccionarlo explícitamente: -@dataclass -class User: - id: int - email: str - is_active: bool = True -``` - -```python -# src/app/domains/users/schemas.py -from pydantic import BaseModel, ConfigDict, Field - -class UserCreate(BaseModel): - # Dejar ``str`` mantiene este snippet listo para usar sin dependencias - # extra. Para usar la validación de email integrada de pydantic, instala - # la dependencia opcional (``pip install 'pydantic[email]'`` — incluye - # ``email-validator``) y cambia ``str`` por ``EmailStr``. - email: str = Field(min_length=3, max_length=320) - is_active: bool = True - -class UserRead(BaseModel): - id: int - email: str - is_active: bool - model_config = ConfigDict(from_attributes=True) +```console +$ fastkit addroute users . --layout=domain ``` -Renombra `Item → User`, `ItemNotFoundError → UserNotFoundError`, `ItemRepository → UserRepository`, `ItemService → UserService` en `models.py`, `schemas.py`, `repository.py`, `service.py` y `router.py`. No olvides cambiar `prefix="/items"` → `prefix="/users"` y `tags=["items"]` → `tags=["users"]` en el router. - -El repository puede mantener el mismo patrón respaldado por `InMemoryStore` — es genérico sobre el tipo de entidad: +`--layout=classic-layer` genera la estructura tradicional de `api/routes`, `crud` y `schemas`. Ese es también el valor por defecto para `classic-layered`, `minimal`, `single-module` y para los proyectos sin metadatos de preset ni de plantilla. Un preset desconocido recurre a `classic-layer` con una advertencia. La opción no cambia el preset del proyecto. -```python -# src/app/domains/users/repository.py -_store: InMemoryStore[User] = InMemoryStore() +### Dominio generado -class UserRepository: - def __init__(self, store: Optional[InMemoryStore[User]] = None) -> None: - self._store = store if store is not None else _store - # ... misma forma que ItemRepository ... +```text +src/app/domains/users/ +├── __init__.py +├── models.py # Users con id y name +├── schemas.py # UsersCreate y UsersRead +├── repository.py # UsersRepository: CRUD tipado en memoria +├── service.py # UsersService y UsersNotFoundError +└── router.py # endpoints y dependencia get_users_service ``` -### 3. Actualiza el `__init__.py` del dominio +El nuevo dominio usa nombres de clase en PascalCase derivados del nombre de la ruta, así que no hace falta inferir un nombre en singular a partir de `users`. Personaliza estos archivos para introducir los campos y el comportamiento que necesita tu concepto de negocio. -El dominio items reexporta sus módulos para que los llamantes puedan escribir `from src.app.domains.items import service`. Refleja eso en users: +El router se registra automáticamente en `src/app/api/router.py` con `prefix="/users"` y `tags=["users"]`. Los routers de dominio usan `APIRouter()` sin esos ajustes. Se conserva el prefijo existente de la aplicación, de modo que se exponen estos endpoints: -```python -# src/app/domains/users/__init__.py -from src.app.domains.users import ( # noqa: F401 - models, - repository, - router, - schemas, - service, -) -``` +| Método | Endpoint | Resultado | +|---|---|---| +| GET | `/api/v1/users` | Listar entidades, 200 | +| GET | `/api/v1/users/{entity_id}` | Leer una entidad, 200 o 404 | +| POST | `/api/v1/users` | Crear una entidad, 201 | +| PUT | `/api/v1/users/{entity_id}` | Reemplazar una entidad, 200 o 404 | +| DELETE | `/api/v1/users/{entity_id}` | Eliminar una entidad, 204 vacío o 404 | -### 4. Registra el router en el agregador +POST y PUT aceptan `{"name": "Alice"}`; `name` debe tener entre 1 y 120 caracteres. El repository asigna un ID empezando por 1. PUT conserva ese ID. No se genera ningún endpoint PATCH. -Este es el **único archivo fuera de `domains/users/` que necesitas tocar**: +El nuevo repository es autocontenido; no usa el almacén `db.memory` del dominio items incluido. Cada instancia explícita del repository tiene su propio almacenamiento, mientras que la instancia por defecto se comparte entre peticiones dentro de este dominio y proceso. Las operaciones CRUD están protegidas por un lock. Los datos se pierden al reiniciar y no se comparten entre procesos worker; reemplaza el repository cuando necesites almacenamiento duradero. -```python -# src/app/api/router.py -from src.app.api import health -from src.app.domains.items import router as items_router -from src.app.domains.users import router as users_router # ← añadir +### Añade tests aislados -api_router = APIRouter() -api_router.include_router(health.router) -api_router.include_router(items_router.router) -api_router.include_router(users_router.router) # ← añadir -``` +El fixture de reinicio específico de items no reinicia el nuevo dominio. Sobrescribe la dependencia de su service con un repository nuevo en cada test: -Tras reiniciar el servidor verás `/api/v1/users` montado en `/docs`. +```python +from src.app.domains.users.repository import UsersRepository +from src.app.domains.users.router import get_users_service +from src.app.domains.users.service import UsersService -### 5. Añade tests +def test_create_user(client): + from src.app.main import app -Usa `tests/test_items.py` como base para crear `tests/test_users.py`: sigue el mismo patrón con `client`, pero llamando a los nuevos endpoints. El fixture `autouse` que reinicia el almacén en `conftest.py` ya mantiene cada test aislado. + service = UsersService(repository=UsersRepository()) + app.dependency_overrides[get_users_service] = lambda: service + try: + response = client.post("/api/v1/users", json={"name": "Alice"}) + assert response.status_code == 201 + assert response.json() == {"id": 1, "name": "Alice"} + finally: + app.dependency_overrides.pop(get_users_service, None) +``` -Si añades un segundo dominio que también use `InMemoryStore`, amplía el fixture para resetear también su store, o mantén un fixture por dominio. +Volver a ejecutar `addroute` conserva los archivos de dominio existentes, crea los que faltan y evita imports y registros duplicados. Los módulos clásicos con el mismo nombre pueden coexistir gracias a alias de import. Los paquetes generados reexportan sus módulos, y los repositories exponen `reset()` para borrar los datos y reiniciar los IDs. El fixture `autouse` del starter llama a `service.reset_store()` para los dominios generados. En proyectos más antiguos, añade esta llamada al fixture de reinicio existente o usa repositories aislados mediante la sobrescritura de dependencias. Comprueba tú mismo las rutas y los métodos HTTP al combinar routers; el generador no detecta endpoints solapados. ## Paso 6: A dónde ir después @@ -389,4 +369,4 @@ Si añades un segundo dominio que también use `InMemoryStore`, amplía el fixtu - **Generación**: `fastkit startdemo fastapi-domain-starter` → `bash scripts/run-server.sh` → docs en `/docs`. - **Estructura**: `core/` para la configuración, `db/` para las abstracciones de persistencia, `domains//` para los bloques de negocio, `api/router.py` como único punto de agregación y `tests/` reflejando los módulos de la aplicación. -- **Añadir un dominio**: copia `items/`, renombra entidad / esquemas / clases, actualiza los re-exports de `__init__.py`, registra el router en `src/app/api/router.py`, añade un módulo de tests. Sin tocar `main.py`. +- **Añadir un dominio**: ejecuta `fastkit addroute users`, personaliza el scaffold CRUD `Users*` generado y añade tests con repositories aislados. El router se registra automáticamente. diff --git a/docs/es/user-guide/adding-routes.md b/docs/es/user-guide/adding-routes.md index 5dd867e..11b0796 100644 --- a/docs/es/user-guide/adding-routes.md +++ b/docs/es/user-guide/adding-routes.md @@ -2,8 +2,91 @@ Aprende a añadir nuevas rutas de API a un proyecto FastAPI existente. +## Elegir el layout de la ruta + +`addroute` acepta `--layout=classic-layer` o `--layout=domain`. Si se omite, +lee `[tool.fastapi-fastkit].preset`: `domain-starter` selecciona `domain`; +`classic-layered`, `minimal` y `single-module` seleccionan `classic-layer`. +Si falta `preset`, infiere el preset a partir de `template`, de modo que los proyectos creados +con `startdemo` o con `init` interactivo también seleccionan `domain` para +`fastapi-domain-starter`. Sin ninguno de los dos valores, selecciona `classic-layer`. Un preset desconocido recurre a `classic-layer` con +una advertencia. Una opción explícita anula esta selección sin cambiar los +metadatos del proyecto. + +```console +$ cd my-api +$ fastkit addroute users . --layout=domain +$ fastkit addroute products . --layout=classic-layer +``` + +El layout seleccionado se muestra antes de la confirmación. Los archivos se colocan en +el paquete real de la aplicación (`src/` o `src/app/`), usando su +entrypoint registrado cuando está disponible. + +## Generar un CRUD de dominio + +En un proyecto `domain-starter`, basta con ejecutar: + +```console +$ fastkit addroute users +``` + +Esto crea los siguientes archivos y registra automáticamente el router del dominio: + +```text +src/app/domains/users/ +├── __init__.py +├── models.py # Users(id: int, name: str) +├── schemas.py # UsersCreate for POST/PUT, UsersRead for responses +├── repository.py # typed CRUD operations and in-memory storage +├── service.py # business logic and UsersNotFoundError +└── router.py # HTTP endpoints and overridable service dependency +``` + +Las clases se llaman `Users`, `UsersCreate`, `UsersRead`, `UsersRepository` +y `UsersService` dentro de cada dominio; los nombres de clase usan PascalCase (`health_checks` pasa a ser `HealthChecks`), sin singularización. +El repositorio asigna IDs empezando por 1. POST y PUT requieren un `name` de entre +1 y 120 caracteres; PUT conserva el ID existente. + +| Método | Ruta del dominio | Resultado | +|---|---|---| +| GET | `/users` | Listar entidades, 200 | +| GET | `/users/{entity_id}` | Leer una entidad, 200 o 404 | +| POST | `/users` | Crear una entidad, 201 | +| PUT | `/users/{entity_id}` | Reemplazar una entidad, 200 o 404 | +| DELETE | `/users/{entity_id}` | Eliminar una entidad, 204 vacío o 404 | + +El prefijo y las etiquetas del dominio se establecen en el router compartido de la API cuando llama a +`include_router`, igual que en el layout clásico. + +El prefijo de API existente de la aplicación sigue vigente (por ejemplo, +`/api/v1/users`). Las rutas de dominio no incluyen PATCH. + +El repositorio generado es autocontenido y protege sus operaciones CRUD con un lock. Su +instancia por defecto se comparte entre peticiones **dentro de este dominio y proceso**. +Los datos se pierden al reiniciar y no se comparten entre procesos worker. Adapta el +repositorio para tener persistencia duradera; no depende de `db.memory` del starter, +de ningún ORM ni de la configuración de base de datos. + +Para pruebas aisladas, inyecta un `UsersRepository` nuevo en `UsersService` +y después sobrescribe la dependencia de FastAPI `get_users_service` del router. +`UsersRepository.reset()` vacía ese repositorio y reinicia los IDs en 1. +`service.reset_store()` reinicia el repositorio por defecto compartido entre peticiones. +Las instancias de repositorio creadas explícitamente tienen almacenamiento independiente. + +## Volver a ejecutar el comando + +Los archivos de módulo existentes se conservan; los que faltan se crean. Los imports y +los registros del router no se duplican. Los módulos con el mismo nombre en layouts distintos +pueden coexistir; los alias de import evitan colisiones de nombres en Python. El generador no +comprueba conflictos de método/ruta HTTP. `--layout` no migra los módulos +existentes. + ## Añadir una ruta básica +Los siguientes ejemplos describen el layout `classic-layer`. Pasa +`--layout=classic-layer` explícitamente cuando uses un proyecto domain-starter. + ### Usar el comando `addroute` El comando `addroute` de FastAPI-fastkit facilita añadir nuevas rutas: diff --git a/docs/es/user-guide/cli-reference.md b/docs/es/user-guide/cli-reference.md index 5ba0703..4a71ada 100644 --- a/docs/es/user-guide/cli-reference.md +++ b/docs/es/user-guide/cli-reference.md @@ -356,6 +356,7 @@ $ fastkit addroute ROUTE_NAME [PROJECT_DIR] [OPTIONS] | Opción | Descripción | Por defecto | |---|---|---| +| `--layout [classic-layer\|domain]` | Estructura del módulo de ruta generado; anula los metadatos del preset | Automático según el preset | | `--help` | Muestra la ayuda del comando | - | #### Ejemplos @@ -391,25 +392,57 @@ $ fastkit addroute users my-api #### Archivos generados -Crea estos archivos en el proyecto: +Con `--layout=classic-layer`, crea estos archivos bajo el paquete de la aplicación: - `src/api/routes/users.py` - Handlers de ruta - `src/crud/users.py` - Operaciones CRUD - `src/schemas/users.py` - Esquemas Pydantic -También actualiza `src/api/api.py` para incluir el nuevo router. +Con `--layout=domain`, crea `domains/users/` con `__init__.py`, +`models.py`, `schemas.py`, `repository.py`, `service.py` y `router.py`. +La entidad tiene `id: int` y `name: str`, con un repositorio en memoria funcional. + +Ambos layouts actualizan el router de API existente y lo conectan a la aplicación +si hace falta. Las rutas siguen el paquete real de la aplicación, incluido `src/app/`. + +Sin la opción, `[tool.fastapi-fastkit].preset = "domain-starter"` selecciona +`domain`. Cuando falta `preset`, el preset se infiere a partir de `template`; +`fastapi-domain-starter` también selecciona `domain`. Los demás presets y los proyectos sin +ninguno de esos metadatos seleccionan `classic-layer`. Un preset desconocido recurre a `classic-layer` con una +advertencia. La selección explícita no cambia el preset del proyecto. + +```console +$ fastkit addroute users . --layout=domain +$ fastkit addroute products . --layout=classic-layer +``` + +El comando muestra el layout seleccionado antes de la confirmación. Volver a ejecutarlo +conserva los archivos existentes, restaura los que faltan y evita registros duplicados +del router. Los módulos con el mismo nombre de layouts distintos pueden coexistir mediante +alias de import. El generador no comprueba conflictos de método/ruta HTTP. +La migración es manual. #### Endpoints generados -Crea endpoints CRUD completos: +El layout de dominio crea estos endpoints (mostrados con `/api/v1` como prefijo +existente de la aplicación): | Método | Endpoint | Descripción | |---|---|---| -| `GET` | `/api/v1/users/` | Obtener todos los usuarios | -| `POST` | `/api/v1/users/` | Crear un usuario nuevo | -| `GET` | `/api/v1/users/{user_id}` | Obtener un usuario concreto | -| `PUT` | `/api/v1/users/{user_id}` | Actualizar un usuario | -| `DELETE` | `/api/v1/users/{user_id}` | Eliminar un usuario | +| `GET` | `/api/v1/users` | Listar entidades, 200 | +| `POST` | `/api/v1/users` | Crear una entidad, 201 | +| `GET` | `/api/v1/users/{entity_id}` | Leer una entidad, 200 o 404 | +| `PUT` | `/api/v1/users/{entity_id}` | Reemplazar una entidad, 200 o 404 | +| `DELETE` | `/api/v1/users/{entity_id}` | Eliminar una entidad, 204 vacío o 404 | + +POST y PUT aceptan un `name` de 1 a 120 caracteres. Los IDs los asigna el +repositorio, y PUT los conserva. No se genera ningún endpoint PATCH para los dominios. +El almacenamiento es local al proceso, se pierde al reiniciar y es independiente entre dominios; +reemplaza el repositorio para tener persistencia duradera. Las pruebas pueden inyectar un +repositorio nuevo a través del servicio y sobrescribir `get_users_service`. + +El layout clásico conserva sus stubs GET/POST/PUT/PATCH/DELETE existentes en +`//` para que los implementes. #### Dónde se inserta el código @@ -430,7 +463,7 @@ api_router.include_router(health.router) ``` Mantén esos comentarios en su sitio cuando edites el archivo — `addroute` -inserta justo encima de `# fastkit:imports` y `# fastkit:routes`. Un +inserta justo debajo 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 diff --git a/docs/fr/reference/translation-status.md b/docs/fr/reference/translation-status.md index 79c6f70..317ab2f 100644 --- a/docs/fr/reference/translation-status.md +++ b/docs/fr/reference/translation-status.md @@ -19,14 +19,14 @@ Les nombres ci-dessous comptent les pages Markdown dans l'arborescence de chaque | Locale | État | Pages Markdown | Notes | |---|---|---:|---| | 🇬🇧 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-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 : +| 🇰🇷 Coréen (`ko`) | ✅ Complet | 29 / 29 | Phases 1 à 3 terminées. Resynchronisé avec les sources anglaises de la v1.5.0 le 2026-10-14 : 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.5.0 le 2026-10-14 : 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.5.0 le 2026-10-14 : 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.5.0 le 2026-10-14 : 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.5.0 le 2026-10-14 : 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.5.0 le 2026-10-14 : 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-10-14, après la publication de la v1.5.0. 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 diff --git a/docs/fr/tutorial/domain-starter.md b/docs/fr/tutorial/domain-starter.md index 7a27b52..ac216fd 100644 --- a/docs/fr/tutorial/domain-starter.md +++ b/docs/fr/tutorial/domain-starter.md @@ -7,7 +7,7 @@ Construisez un service FastAPI de taille moyenne avec l'organisation moderne rec - Générer un projet avec `fastkit startdemo fastapi-domain-starter` - Le rôle de `core`, `db`, `domains` et `tests` dans la disposition - Comment un domaine s'organise en `router → service → repository → schemas → models` -- Le contrat pour ajouter un nouveau domaine (copier le dossier items, enregistrer le routeur) +- Ajouter un nouveau domaine avec `fastkit addroute` et isoler son stockage dans les tests - Comment le point d'extrémité `/health` fourni et le CRUD `/api/v1/items` s'intègrent à l'application ## Prérequis @@ -76,7 +76,7 @@ orders-api/ │ ├── schemas.py # ItemCreate, ItemRead (Pydantic) │ ├── repository.py # ItemRepository au-dessus de InMemoryStore │ ├── service.py # ItemService + ItemNotFoundError -│ └── router.py # APIRouter(prefix="/items") +│ └── router.py # points d'extrémité APIRouter └── tests/ ├── __init__.py ├── conftest.py # fixture TestClient, remise à zéro du store @@ -138,7 +138,7 @@ Deux pièces : # src/app/api/router.py api_router = APIRouter() api_router.include_router(health.router) -api_router.include_router(items_router.router) +api_router.include_router(items_router.router, prefix="/items", tags=["items"]) ``` ```python @@ -166,7 +166,7 @@ Le **sens des dépendances** suit `router → service → repository → store`. Le dossier reflète la structure de l'application à l'exécution : un module de test par surface de comportement importante. Le starter fournit : -- `conftest.py` — fixture autouse qui réinitialise le magasin d'items entre les tests, plus une fixture `client` qui enveloppe `TestClient(app)`. +- `conftest.py` — fixture autouse qui, entre les tests, réinitialise le magasin d'items et appelle `service.reset_store()` pour les domaines générés, plus une fixture `client` qui enveloppe `TestClient(app)`. - `test_health.py` — vérifie que `GET /api/v1/health` renvoie 200 + `{"status": "ok"}`. - `test_items.py` — couverture CRUD complète des points d'extrémité items, y compris un 404 pour des ids inconnus et un 422 pour une charge utile invalide. @@ -245,7 +245,7 @@ Le routeur est la seule pièce qui connaît HTTP. Notez qu'il prend le service s ```python # src/app/domains/items/router.py -router = APIRouter(prefix="/items", tags=["items"]) +router = APIRouter() def get_item_service() -> ItemService: return ItemService() @@ -258,6 +258,13 @@ def get_item(item_id: int, service: ItemService = Depends(get_item_service)) -> raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(exc)) ``` +Le routeur d'API partagé définit le préfixe et les tags du domaine : + +```python +# src/app/api/router.py +api_router.include_router(items_router.router, prefix="/items", tags=["items"]) +``` + Le routeur complet expose : | Méthode | Chemin | Ce qu'il fait | @@ -286,99 +293,71 @@ $ curl http://127.0.0.1:8000/api/v1/items/999 ## Étape 5 : Ajouter votre prochain domaine -Le starter est conçu pour que **l'ajout d'un domaine reste surtout une opération de copie puis de renommage**. Supposons que vous vouliez un domaine `users` aux côtés de `items` : - -### 1. Copier le dossier `items/` +Depuis la racine du projet généré, ajoutez un domaine `users` : ```console -$ cp -r src/app/domains/items src/app/domains/users +$ fastkit addroute users ``` -### 2. Réécrire l'entité, les schémas et les noms de classe de chaque fichier - -```python -# src/app/domains/users/models.py -from dataclasses import dataclass +La commande lit `[tool.fastapi-fastkit].preset`, ou le déduit de `template` lorsque `preset` est absent. Les projets créés avec `startdemo`, `init` interactif ou `init --config` choisissent donc la disposition domaine pour domain-starter. La disposition sélectionnée est affichée avant la confirmation. Vous pouvez aussi la choisir explicitement : -@dataclass -class User: - id: int - email: str - is_active: bool = True -``` - -```python -# src/app/domains/users/schemas.py -from pydantic import BaseModel, ConfigDict, Field - -class UserCreate(BaseModel): - # Garder ``str`` permet de réutiliser l'exemple tel quel. - # Si vous préférez la validation d'e-mail intégrée à Pydantic, - # installez la dépendance optionnelle - # (``pip install 'pydantic[email]'`` — qui ajoute ``email-validator``) - # puis remplacez ``str`` par ``EmailStr``. - email: str = Field(min_length=3, max_length=320) - is_active: bool = True - -class UserRead(BaseModel): - id: int - email: str - is_active: bool - model_config = ConfigDict(from_attributes=True) +```console +$ fastkit addroute users . --layout=domain ``` -Renommez `Item → User`, `ItemNotFoundError → UserNotFoundError`, `ItemRepository → UserRepository`, `ItemService → UserService` dans `models.py`, `schemas.py`, `repository.py`, `service.py` et `router.py`. N'oubliez pas `prefix="/items"` → `prefix="/users"` et `tags=["items"]` → `tags=["users"]` dans le routeur. - -Le repository peut conserver le même motif appuyé sur `InMemoryStore` — il est générique sur le type d'entité : +`--layout=classic-layer` génère la structure traditionnelle `api/routes`, `crud` et `schemas`. C'est aussi la valeur par défaut pour `classic-layered`, `minimal`, `single-module` et les projets sans métadonnées de préréglage ni de modèle. Un préréglage inconnu se rabat sur `classic-layer` avec un avertissement. L'option ne modifie pas le préréglage du projet. -```python -# src/app/domains/users/repository.py -_store: InMemoryStore[User] = InMemoryStore() +### Domaine généré -class UserRepository: - def __init__(self, store: Optional[InMemoryStore[User]] = None) -> None: - self._store = store if store is not None else _store - # ... same shape as ItemRepository ... +```text +src/app/domains/users/ +├── __init__.py +├── models.py # Users avec id et name +├── schemas.py # UsersCreate et UsersRead +├── repository.py # UsersRepository : CRUD typé en mémoire +├── service.py # UsersService et UsersNotFoundError +└── router.py # points d'extrémité et dépendance get_users_service ``` -### 3. Mettre à jour le `__init__.py` du domaine +Le nouveau domaine utilise des noms de classe en PascalCase dérivés du nom de la route : inutile donc de déduire un nom au singulier à partir de `users`. Adaptez ces fichiers pour introduire les champs et le comportement qu'exige votre concept métier. -Le domaine items réexporte ses modules pour que les appelants puissent écrire `from src.app.domains.items import service`. Faites pareil pour users : +Le routeur est enregistré automatiquement dans `src/app/api/router.py` avec `prefix="/users"` et `tags=["users"]`. Les routeurs de domaine utilisent `APIRouter()` sans ces paramètres. Le préfixe existant de l'application est conservé, ce qui expose ces points d'extrémité : -```python -# src/app/domains/users/__init__.py -from src.app.domains.users import ( # noqa: F401 - models, - repository, - router, - schemas, - service, -) -``` +| Méthode | Point d'extrémité | Résultat | +|--------|----------|--------| +| GET | `/api/v1/users` | Lister les entités, 200 | +| GET | `/api/v1/users/{entity_id}` | Lire une entité, 200 ou 404 | +| POST | `/api/v1/users` | Créer une entité, 201 | +| PUT | `/api/v1/users/{entity_id}` | Remplacer une entité, 200 ou 404 | +| DELETE | `/api/v1/users/{entity_id}` | Supprimer une entité, 204 vide ou 404 | -### 4. Enregistrer le routeur dans l'agrégateur +POST et PUT acceptent `{"name": "Alice"}` ; `name` doit compter de 1 à 120 caractères. Le repository attribue un ID à partir de 1. PUT conserve cet ID. Aucun point d'extrémité PATCH n'est généré. -C'est le **seul fichier en dehors de `domains/users/` que vous devez toucher** : +Le nouveau repository est autonome ; il n'utilise pas le magasin `db.memory` du domaine items fourni. Chaque instance de repository créée explicitement dispose de son propre stockage, tandis que l'instance par défaut est partagée entre les requêtes de ce domaine et de ce processus. Les opérations CRUD sont protégées par un verrou. Les données sont perdues au redémarrage et ne sont pas partagées entre les processus workers ; remplacez le repository lorsque vous avez besoin d'un stockage durable. -```python -# src/app/api/router.py -from src.app.api import health -from src.app.domains.items import router as items_router -from src.app.domains.users import router as users_router # ← add +### Ajouter des tests isolés -api_router = APIRouter() -api_router.include_router(health.router) -api_router.include_router(items_router.router) -api_router.include_router(users_router.router) # ← add -``` +La fixture de réinitialisation propre à items ne réinitialise pas le nouveau domaine. Surchargez sa dépendance de service avec un repository neuf pour chaque test : -Après un redémarrage du serveur, vous verrez `/api/v1/users` monté dans `/docs`. +```python +from src.app.domains.users.repository import UsersRepository +from src.app.domains.users.router import get_users_service +from src.app.domains.users.service import UsersService -### 5. Ajouter des tests +def test_create_user(client): + from src.app.main import app -Calquez `tests/test_items.py` en `tests/test_users.py` — même forme orientée client, pointant simplement sur les nouveaux points d'extrémité. La fixture autouse de réinitialisation du magasin dans `conftest.py` isole déjà chaque test. + service = UsersService(repository=UsersRepository()) + app.dependency_overrides[get_users_service] = lambda: service + try: + response = client.post("/api/v1/users", json={"name": "Alice"}) + assert response.status_code == 201 + assert response.json() == {"id": 1, "name": "Alice"} + finally: + app.dependency_overrides.pop(get_users_service, None) +``` -Si vous ajoutez un deuxième domaine qui utilise aussi `InMemoryStore`, élargissez la fixture pour réinitialiser également son magasin, ou conservez une fixture par domaine. +Relancer `addroute` conserve les fichiers de domaine existants, crée les fichiers manquants et évite les imports et enregistrements en double. Des modules classiques portant le même nom peuvent coexister grâce à des alias d'import. Les packages générés réexportent leurs modules, et les repositories exposent `reset()` pour effacer les données et redémarrer les ID. La fixture autouse du starter appelle `service.reset_store()` pour les domaines générés. Dans les projets plus anciens, ajoutez cet appel à la fixture de réinitialisation existante ou utilisez des repositories isolés via des surcharges de dépendances. Vérifiez vous-même les chemins et méthodes HTTP lorsque vous combinez des routeurs ; le générateur ne détecte pas les points d'extrémité qui se chevauchent. ## Étape 6 : Aller plus loin @@ -390,4 +369,4 @@ Si vous ajoutez un deuxième domaine qui utilise aussi `InMemoryStore`, élargis - **Génération** : `fastkit startdemo fastapi-domain-starter` → `bash scripts/run-server.sh` → documentation à `/docs`. - **Disposition** : `core/` pour la configuration, `db/` pour les abstractions de persistance, `domains//` pour les tranches métier, `api/router.py` comme point d'agrégation unique, `tests/` qui reflète les modules d'exécution. -- **Ajouter un domaine** : copier `items/`, renommer entité / schémas / classes, mettre à jour les réexportations de `__init__.py`, enregistrer le routeur dans `src/app/api/router.py`, ajouter un module de test. Aucune édition dans `main.py`. +- **Ajouter un domaine** : lancer `fastkit addroute users`, adapter l'ossature CRUD `Users*` générée et ajouter des tests avec des repositories isolés. Le routeur est enregistré automatiquement. diff --git a/docs/fr/user-guide/adding-routes.md b/docs/fr/user-guide/adding-routes.md index cadc5d9..bf035b0 100644 --- a/docs/fr/user-guide/adding-routes.md +++ b/docs/fr/user-guide/adding-routes.md @@ -2,8 +2,64 @@ Apprenez à ajouter de nouvelles routes d'API à votre projet FastAPI existant. +## Choisir la disposition de la route + +`addroute` accepte `--layout=classic-layer` ou `--layout=domain`. Sans cette option, il lit `[tool.fastapi-fastkit].preset` : `domain-starter` sélectionne `domain` ; `classic-layered`, `minimal` et `single-module` sélectionnent `classic-layer`. Si `preset` est absent, il déduit le préréglage de `template`, si bien que les projets créés avec `startdemo` ou `init` interactif sélectionnent aussi `domain` pour `fastapi-domain-starter`. En l'absence de ces deux valeurs, il sélectionne `classic-layer`. Un préréglage inconnu se rabat sur `classic-layer` avec un avertissement. Une option explicite remplace cette sélection sans modifier les métadonnées du projet. + +```console +$ cd my-api +$ fastkit addroute users . --layout=domain +$ fastkit addroute products . --layout=classic-layer +``` + +La disposition sélectionnée est affichée avant la confirmation. Les fichiers sont placés sous le package réel de l'application (`src/` ou `src/app/`), en s'appuyant sur son point d'entrée enregistré lorsqu'il est disponible. + +## Générer un CRUD de domaine + +Pour un projet `domain-starter`, lancez simplement : + +```console +$ fastkit addroute users +``` + +Cette commande crée les fichiers suivants et enregistre automatiquement le routeur du domaine : + +```text +src/app/domains/users/ +├── __init__.py +├── models.py # Users(id: int, name: str) +├── schemas.py # UsersCreate pour POST/PUT, UsersRead pour les réponses +├── repository.py # opérations CRUD typées et stockage en mémoire +├── service.py # logique métier et UsersNotFoundError +└── router.py # points d'extrémité HTTP et dépendance de service surchargeable +``` + +Dans chaque domaine, les classes s'appellent `Users`, `UsersCreate`, `UsersRead`, `UsersRepository` et `UsersService` ; les noms de classe utilisent le PascalCase (`health_checks` devient `HealthChecks`), sans mise au singulier. Le repository attribue des ID à partir de 1. POST et PUT exigent un `name` de 1 à 120 caractères ; PUT conserve l'ID existant. + +| Méthode | Chemin du domaine | Résultat | +|--------|-------------|--------| +| GET | `/users` | Lister les entités, 200 | +| GET | `/users/{entity_id}` | Lire une entité, 200 ou 404 | +| POST | `/users` | Créer une entité, 201 | +| PUT | `/users/{entity_id}` | Remplacer une entité, 200 ou 404 | +| DELETE | `/users/{entity_id}` | Supprimer une entité, 204 vide ou 404 | + +Le préfixe et les tags du domaine sont définis dans le routeur d'API partagé lorsqu'il appelle `include_router`, comme dans la disposition classique. + +Le préfixe d'API existant de l'application reste en vigueur (par exemple `/api/v1/users`). Les routes de domaine n'incluent pas PATCH. + +Le repository généré est autonome et protège ses opérations CRUD par un verrou. Son instance par défaut est partagée entre les requêtes **au sein de ce domaine et de ce processus**. Les données sont perdues au redémarrage et ne sont pas partagées entre les processus workers. Adaptez le repository pour une persistance durable ; il ne dépend ni du `db.memory` du starter, ni d'un ORM, ni d'une configuration de base de données. + +Pour des tests isolés, injectez un `UsersRepository` neuf dans `UsersService`, puis surchargez la dépendance FastAPI `get_users_service` du routeur. `UsersRepository.reset()` vide ce repository et redémarre les ID à 1. `service.reset_store()` réinitialise le repository par défaut partagé entre les requêtes. Les instances de repository créées explicitement disposent de leur propre stockage. + +## Relancer la commande + +Les fichiers de module existants sont conservés ; les fichiers manquants sont créés. Les imports et les enregistrements de routeurs ne sont pas dupliqués. Des modules de même nom dans des dispositions différentes peuvent coexister ; des alias d'import évitent les collisions de noms Python. Le générateur ne vérifie pas les conflits de méthode/chemin HTTP. `--layout` ne migre pas les modules existants. + ## Ajout de route basique +Les exemples suivants décrivent la disposition `classic-layer`. Passez explicitement `--layout=classic-layer` lorsque vous utilisez un projet domain-starter. + ### Utiliser la commande `addroute` La commande `addroute` de FastAPI-fastkit facilite l'ajout de nouvelles routes : diff --git a/docs/fr/user-guide/cli-reference.md b/docs/fr/user-guide/cli-reference.md index bfa647f..c34d9cc 100644 --- a/docs/fr/user-guide/cli-reference.md +++ b/docs/fr/user-guide/cli-reference.md @@ -311,6 +311,7 @@ $ fastkit addroute ROUTE_NAME [PROJECT_DIR] [OPTIONS] | Option | Description | Défaut | |--------|-------------|---------| +| `--layout [classic-layer\|domain]` | Structure du module de route généré ; remplace les métadonnées de préréglage | Automatique selon le préréglage | | `--help` | Afficher l'aide de la commande | - | #### Exemples @@ -346,25 +347,40 @@ $ fastkit addroute users my-api #### Fichiers générés -Crée ces fichiers dans le projet : +Avec `--layout=classic-layer`, crée ces fichiers sous le package de l'application : - `src/api/routes/users.py` — gestionnaires de route - `src/crud/users.py` — opérations CRUD - `src/schemas/users.py` — schémas Pydantic -Met aussi à jour `src/api/api.py` pour inclure le nouveau routeur. +Avec `--layout=domain`, crée `domains/users/` contenant `__init__.py`, `models.py`, `schemas.py`, `repository.py`, `service.py` et `router.py`. L'entité possède `id: int` et `name: str`, avec un repository en mémoire fonctionnel. + +Les deux dispositions mettent à jour le routeur d'API existant et le relient à l'application si nécessaire. Les chemins suivent le package réel de l'application, y compris `src/app/`. + +Sans l'option, `[tool.fastapi-fastkit].preset = "domain-starter"` sélectionne `domain`. Lorsque `preset` est absent, le préréglage est déduit de `template` ; `fastapi-domain-starter` sélectionne aussi `domain`. Les autres préréglages et les projets sans aucune de ces deux métadonnées sélectionnent `classic-layer`. Un préréglage inconnu se rabat sur `classic-layer` avec un avertissement. Une sélection explicite ne modifie pas le préréglage du projet. + +```console +$ fastkit addroute users . --layout=domain +$ fastkit addroute products . --layout=classic-layer +``` + +La commande affiche la disposition sélectionnée avant la confirmation. La relancer conserve les fichiers existants, restaure les fichiers manquants et évite les enregistrements de routeurs en double. Des modules de même nom issus de dispositions différentes peuvent coexister grâce à des alias d'import. Le générateur ne vérifie pas les conflits de méthode/chemin HTTP. La migration est manuelle. #### Points d'extrémité générés -Crée des points d'extrémité CRUD complets : +La disposition domaine crée ces points d'extrémité (affichés avec `/api/v1` comme préfixe existant de l'application) : | Méthode | Point d'extrémité | Description | |--------|----------|-------------| -| `GET` | `/api/v1/users/` | Récupérer tous les utilisateurs | -| `POST` | `/api/v1/users/` | Créer un nouvel utilisateur | -| `GET` | `/api/v1/users/{user_id}` | Récupérer un utilisateur précis | -| `PUT` | `/api/v1/users/{user_id}` | Mettre à jour un utilisateur | -| `DELETE` | `/api/v1/users/{user_id}` | Supprimer un utilisateur | +| `GET` | `/api/v1/users` | Lister les entités, 200 | +| `POST` | `/api/v1/users` | Créer une entité, 201 | +| `GET` | `/api/v1/users/{entity_id}` | Lire une entité, 200 ou 404 | +| `PUT` | `/api/v1/users/{entity_id}` | Remplacer une entité, 200 ou 404 | +| `DELETE` | `/api/v1/users/{entity_id}` | Supprimer une entité, 204 vide ou 404 | + +POST et PUT acceptent un `name` de 1 à 120 caractères. Les ID sont attribués par le repository, et PUT les conserve. Aucun point d'extrémité PATCH n'est généré pour les domaines. Le stockage est local au processus, perdu au redémarrage et indépendant d'un domaine à l'autre ; remplacez le repository pour une persistance durable. Les tests peuvent injecter un repository neuf via le service et surcharger `get_users_service`. + +La disposition classique conserve ses ébauches GET/POST/PUT/PATCH/DELETE existantes sur `//`, à implémenter vous-même. #### Où le code est inséré @@ -382,7 +398,7 @@ 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. +Conservez ces commentaires lorsque vous modifiez le fichier — `addroute` insère directement en dessous 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. diff --git a/docs/ja/reference/translation-status.md b/docs/ja/reference/translation-status.md index a29cdff..e618571 100644 --- a/docs/ja/reference/translation-status.md +++ b/docs/ja/reference/translation-status.md @@ -19,14 +19,14 @@ FastAPI-fastkit のドキュメントは複数の言語でビルドされます | ロケール | ステータス | Markdown ページ | 備考 | |---|---|---:|---| | 🇬🇧 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-10-11 (v1.4.1 リリース後)。翻訳対象の 6 つのロケールすべてを英語原典と同じ水準に揃えました: v1.4.0 で追加された 3 つのチュートリアルを翻訳し、最初の翻訳以降にずれていたすべてのページを再同期しました。リリースの途中では翻訳が遅れることもあります — 翻訳ページが英語と食い違う場合は英語を信頼してください。* この表は手動で管理されています。リポジトリルートで現在の状態を再カウントしたい場合は、次のコマンドを実行してください: +| 🇰🇷 Korean (`ko`) | ✅ 完了 | 29 / 29 | Phase 1〜3 は完了しています。2026-10-14 に v1.5.0 の英語原典と再同期しました: 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-14 に v1.5.0 の英語原典と再同期しました: 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-14 に v1.5.0 の英語原典と再同期しました: 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-14 に v1.5.0 の英語原典と再同期しました: 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-14 に v1.5.0 の英語原典と再同期しました: 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-14 に v1.5.0 の英語原典と再同期しました: v1.4.0 で追加された 3 つのチュートリアル (`tutorial/auth-jwt.md`、`tutorial/sqlmodel.md`、`tutorial/llm-agent.md`) を翻訳し、最初の翻訳以降に英語原典とずれていたページも元どおりに揃えました。`docs/de/changelog.md` は英語の `CHANGELOG.md` を意図的に再利用しています。 | + +*スナップショット検証日: 2026-10-14 (v1.5.0 リリース後)。翻訳対象の 6 つのロケールすべてを英語原典と同じ水準に揃えました: v1.4.0 で追加された 3 つのチュートリアルを翻訳し、最初の翻訳以降にずれていたすべてのページを再同期しました。リリースの途中では翻訳が遅れることもあります — 翻訳ページが英語と食い違う場合は英語を信頼してください。* この表は手動で管理されています。リポジトリルートで現在の状態を再カウントしたい場合は、次のコマンドを実行してください: ```console $ for loc in en ko ja zh es fr de; do diff --git a/docs/ja/tutorial/domain-starter.md b/docs/ja/tutorial/domain-starter.md index 16e60b1..77c3436 100644 --- a/docs/ja/tutorial/domain-starter.md +++ b/docs/ja/tutorial/domain-starter.md @@ -7,7 +7,7 @@ - `fastkit startdemo fastapi-domain-starter` でプロジェクトを生成する - レイアウトにおける `core`、`db`、`domains`、`tests` の役割 - ドメインを router → service → repository → schemas → models に分割する考え方 -- 新しいドメインを追加するための手順 (items フォルダをコピーし、ルーターを登録) +- `fastkit addroute` で新しいドメインを追加し、テストでそのストレージを分離する方法 - 付属の `/health` エンドポイントと `/api/v1/items` CRUD が、どのようにアプリに組み込まれているか ## 前提条件 @@ -76,7 +76,7 @@ orders-api/ │ ├── schemas.py # ItemCreate、ItemRead (pydantic) │ ├── repository.py # InMemoryStore を包む ItemRepository │ ├── service.py # ItemService + ItemNotFoundError -│ └── router.py # APIRouter(prefix="/items") +│ └── router.py # APIRouter エンドポイント └── tests/ ├── __init__.py ├── conftest.py # TestClient フィクスチャ、ストアリセット @@ -138,7 +138,7 @@ class InMemoryStore(Generic[T]): # src/app/api/router.py api_router = APIRouter() api_router.include_router(health.router) -api_router.include_router(items_router.router) +api_router.include_router(items_router.router, prefix="/items", tags=["items"]) ``` ```python @@ -166,7 +166,7 @@ app.include_router(api_router, prefix=settings.API_V1_PREFIX) ランタイムのレイアウトを反映する形で、振る舞いを固定したいポイントごとに 1 つずつテストモジュールを置きます。スターターに含まれているものは次のとおりです: -- `conftest.py` — テストの間に items ストアをリセットする autouse フィクスチャ、および `TestClient(app)` をラップした `client` フィクスチャ。 +- `conftest.py` — テストの間に items ストアをリセットし、生成されたドメインに対して `service.reset_store()` を呼び出す autouse フィクスチャ、および `TestClient(app)` をラップした `client` フィクスチャ。 - `test_health.py` — `GET /api/v1/health` が 200 と `{"status": "ok"}` を返すことを検証。 - `test_items.py` — items エンドポイントの完全な CRUD をカバー。未知の id に対する 404、不正なペイロードに対する 422 を含む。 @@ -245,7 +245,7 @@ router だけが HTTP を知る部分です。FastAPI の `Depends(...)` を介 ```python # src/app/domains/items/router.py -router = APIRouter(prefix="/items", tags=["items"]) +router = APIRouter() def get_item_service() -> ItemService: return ItemService() @@ -258,6 +258,13 @@ def get_item(item_id: int, service: ItemService = Depends(get_item_service)) -> raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(exc)) ``` +ドメインのプレフィックスとタグは、共有の API ルーターが設定します: + +```python +# src/app/api/router.py +api_router.include_router(items_router.router, prefix="/items", tags=["items"]) +``` + 完全な router は次を公開します: | メソッド | パス | 動作 | @@ -286,98 +293,72 @@ $ curl http://127.0.0.1:8000/api/v1/items/999 ## ステップ 5: 次のドメインを追加する -スターターは **ドメインの追加がコピーとリネームの操作で済む** ように設計されています。たとえば `items` の隣に `users` ドメインを置きたい場合: - -### 1. `items/` フォルダをコピー +生成したプロジェクトのルートで、`users` ドメインを追加します: ```console -$ cp -r src/app/domains/items src/app/domains/users +$ fastkit addroute users ``` -### 2. エンティティ、スキーマ、各ファイルのクラス名を書き換える - -```python -# src/app/domains/users/models.py -from dataclasses import dataclass +このコマンドは `[tool.fastapi-fastkit].preset` を読み取り、`preset` がない場合は `template` からプリセットを推定します。そのため、`startdemo`、対話式の `init`、`init --config` で作成したプロジェクトでは、domain-starter に対してドメインレイアウトが選ばれます。選択されたレイアウトは確認の前に表示されます。 +明示的に指定することもできます: -@dataclass -class User: - id: int - email: str - is_active: bool = True -``` - -```python -# src/app/domains/users/schemas.py -from pydantic import BaseModel, ConfigDict, Field - -class UserCreate(BaseModel): - # 単純な ``str`` のままにしておけば、このスニペットはそのまま動きます。 - # pydantic 組み込みのメール検証を使いたい場合は、オプション依存を追加で - # 入れて (``pip install 'pydantic[email]'`` — ``email-validator`` が - # 入ります)、``str`` を ``EmailStr`` に切り替えてください。 - email: str = Field(min_length=3, max_length=320) - is_active: bool = True - -class UserRead(BaseModel): - id: int - email: str - is_active: bool - model_config = ConfigDict(from_attributes=True) +```console +$ fastkit addroute users . --layout=domain ``` -`Item → User`、`ItemNotFoundError → UserNotFoundError`、`ItemRepository → UserRepository`、`ItemService → UserService` を `models.py`、`schemas.py`、`repository.py`、`service.py`、`router.py` の全体でリネームします。router 内の `prefix="/items"` を `prefix="/users"` に、`tags=["items"]` を `tags=["users"]` に変更するのも忘れずに。 - -repository は同じく `InMemoryStore` ベースのパターンをそのまま流用できます — エンティティ型に対してジェネリックだからです: +`--layout=classic-layer` を指定すると、従来の `api/routes`、`crud`、`schemas` 構造が生成されます。これは `classic-layered`、`minimal`、`single-module` と、プリセットやテンプレートのメタデータを持たないプロジェクトでのデフォルトでもあります。未知のプリセットは警告を出したうえで `classic-layer` にフォールバックします。このオプションはプロジェクトのプリセットを変更しません。 -```python -# src/app/domains/users/repository.py -_store: InMemoryStore[User] = InMemoryStore() +### 生成されるドメイン -class UserRepository: - def __init__(self, store: Optional[InMemoryStore[User]] = None) -> None: - self._store = store if store is not None else _store - # ... ItemRepository と同じ形 ... +```text +src/app/domains/users/ +├── __init__.py +├── models.py # id と name を持つ Users +├── schemas.py # UsersCreate と UsersRead +├── repository.py # UsersRepository: 型付きのインメモリ CRUD +├── service.py # UsersService と UsersNotFoundError +└── router.py # エンドポイントと get_users_service 依存性 ``` -### 3. ドメインの `__init__.py` を更新する +新しいドメインはルート名から導いた PascalCase のクラス名を使うため、`users` から単数形の名前を推測する必要はありません。ビジネス概念に必要なフィールドと振る舞いを取り入れるよう、これらのファイルをカスタマイズしてください。 -items ドメインは、呼び出し側が `from src.app.domains.items import service` と書けるよう、自身のモジュールを再エクスポートしています。users でも同様にミラーします: +router は `prefix="/users"` と `tags=["users"]` を付けて `src/app/api/router.py` に自動で登録されます。ドメインの router はこれらの設定を持たない `APIRouter()` を使います。既存のアプリケーションプレフィックスはそのまま維持され、次のエンドポイントが公開されます: -```python -# src/app/domains/users/__init__.py -from src.app.domains.users import ( # noqa: F401 - models, - repository, - router, - schemas, - service, -) -``` +| メソッド | エンドポイント | 結果 | +|---|---|---| +| GET | `/api/v1/users` | エンティティの一覧、200 | +| GET | `/api/v1/users/{entity_id}` | エンティティの取得、200 または 404 | +| POST | `/api/v1/users` | エンティティの作成、201 | +| PUT | `/api/v1/users/{entity_id}` | エンティティの置換、200 または 404 | +| DELETE | `/api/v1/users/{entity_id}` | エンティティの削除、空の 204 または 404 | -### 4. 集約点に router を登録する +POST と PUT は `{"name": "Alice"}` を受け付けます。`name` は 1〜120 文字でなければなりません。repository は 1 から始まる ID を割り当て、PUT はその ID を維持します。PATCH エンドポイントは生成されません。 -これが **`domains/users/` の外で唯一触る必要のあるファイル** です: +新しい repository は自己完結しており、付属の items ドメインが使う `db.memory` ストアは使いません。明示的に作成した repository インスタンスはそれぞれ独立したストレージを持ち、デフォルトのインスタンスはこのドメインとプロセスの中でリクエスト間で共有されます。CRUD 操作はロックで保護されています。データは再起動で失われ、ワーカープロセス間では共有されません。永続的なストレージが必要な場合は repository を置き換えてください。 -```python -# src/app/api/router.py -from src.app.api import health -from src.app.domains.items import router as items_router -from src.app.domains.users import router as users_router # ← 追加 +### 分離されたテストを追加する -api_router = APIRouter() -api_router.include_router(health.router) -api_router.include_router(items_router.router) -api_router.include_router(users_router.router) # ← 追加 -``` +items 専用のリセットフィクスチャは、新しいドメインをリセットしません。テストごとに新しい repository を使うよう、service の依存性をオーバーライドしてください: -サーバーを再起動すると、`/docs` に `/api/v1/users` がマウントされます。 +```python +from src.app.domains.users.repository import UsersRepository +from src.app.domains.users.router import get_users_service +from src.app.domains.users.service import UsersService -### 5. テストを追加する +def test_create_user(client): + from src.app.main import app -`tests/test_items.py` をミラーして `tests/test_users.py` を作成します — クライアント駆動の同じ形のまま、新しいエンドポイントを叩くだけです。`conftest.py` の autouse なストアリセットフィクスチャが、各テストを引き続き分離してくれます。 + service = UsersService(repository=UsersRepository()) + app.dependency_overrides[get_users_service] = lambda: service + try: + response = client.post("/api/v1/users", json={"name": "Alice"}) + assert response.status_code == 201 + assert response.json() == {"id": 1, "name": "Alice"} + finally: + app.dependency_overrides.pop(get_users_service, None) +``` -`InMemoryStore` を使う 2 つ目のドメインを追加する場合は、フィクスチャを拡張してそのストアもリセットするか、ドメインごとにフィクスチャを分けてください。 +`addroute` を再実行すると、既存のドメインファイルは保持され、不足しているファイルが作成され、import と登録の重複は避けられます。同名のクラシックモジュールは、import エイリアスによって共存できます。生成されたパッケージは自身のモジュールを再エクスポートし、repository はデータを消去して ID を最初から振り直す `reset()` を公開します。スターターの autouse フィクスチャは、生成されたドメインに対して `service.reset_store()` を呼び出します。古いプロジェクトでは、既存のリセットフィクスチャにこの呼び出しを追加するか、依存性のオーバーライドを通じて分離された repository を使ってください。router を組み合わせるときは、HTTP のパスとメソッドを自分で確認してください。ジェネレーターは重複するエンドポイントを検出しません。 ## ステップ 6: 次に学ぶこと @@ -389,4 +370,4 @@ api_router.include_router(users_router.router) # ← 追加 - **生成**: `fastkit startdemo fastapi-domain-starter` → `bash scripts/run-server.sh` → ドキュメントは `/docs`。 - **レイアウト**: 設定は `core/`、永続化抽象化は `db/`、ビジネススライスは `domains//`、唯一の集約点が `api/router.py`、`tests/` はランタイムモジュールをミラー。 -- **ドメインの追加**: `items/` をコピー、エンティティ / スキーマ / クラスをリネーム、`__init__.py` の再エクスポートを更新、`src/app/api/router.py` で router を登録、テストモジュールを追加。`main.py` の編集は不要です。 +- **ドメインの追加**: `fastkit addroute users` を実行し、生成された `Users*` CRUD スキャフォールドをカスタマイズし、分離された repository を使うテストを追加します。router は自動で登録されます。 diff --git a/docs/ja/user-guide/adding-routes.md b/docs/ja/user-guide/adding-routes.md index d2e1078..4a72e65 100644 --- a/docs/ja/user-guide/adding-routes.md +++ b/docs/ja/user-guide/adding-routes.md @@ -2,8 +2,70 @@ 既存の FastAPI プロジェクトに新しい API ルートを追加する方法を学びます。 +## ルートレイアウトを選ぶ + +`addroute` は `--layout=classic-layer` または `--layout=domain` を受け付けます。省略した場合は `[tool.fastapi-fastkit].preset` を読み取ります: `domain-starter` なら `domain` を、`classic-layered`、`minimal`、`single-module` なら `classic-layer` を選びます。 +`preset` がない場合は `template` からプリセットを推定するため、`startdemo` や対話式の `init` で作成したプロジェクトでも、`fastapi-domain-starter` に対して `domain` が選ばれます。どちらの値もなければ `classic-layer` を選びます。未知のプリセットは警告を出したうえで `classic-layer` にフォールバックします。明示的なオプションは、プロジェクトのメタデータを変更せずにこの選択を上書きします。 + +```console +$ cd my-api +$ fastkit addroute users . --layout=domain +$ fastkit addroute products . --layout=classic-layer +``` + +選択されたレイアウトは確認の前に表示されます。ファイルはアプリケーションの実際のパッケージ (`src/` または `src/app/`) の下に配置され、記録されたエントリポイントがあればそれを使います。 + +## ドメイン CRUD を生成する + +`domain-starter` プロジェクトでは、次のコマンドを実行するだけです: + +```console +$ fastkit addroute users +``` + +これにより次のファイルが作成され、ドメインのルーターが自動で登録されます: + +```text +src/app/domains/users/ +├── __init__.py +├── models.py # Users(id: int, name: str) +├── schemas.py # POST/PUT 用の UsersCreate、レスポンス用の UsersRead +├── repository.py # 型付きの CRUD 操作とインメモリストレージ +├── service.py # ビジネスロジックと UsersNotFoundError +└── router.py # HTTP エンドポイントとオーバーライド可能な service 依存性 +``` + +各ドメインのクラス名は `Users`、`UsersCreate`、`UsersRead`、`UsersRepository`、`UsersService` になります。クラス名は単数形に変換されず、PascalCase になります (`health_checks` は `HealthChecks` になります)。 +repository は 1 から始まる ID を割り当てます。POST と PUT には 1〜120 文字の `name` が必要で、PUT は既存の ID を維持します。 + +| メソッド | ドメインパス | 結果 | +|---|---|---| +| GET | `/users` | エンティティの一覧、200 | +| GET | `/users/{entity_id}` | エンティティの取得、200 または 404 | +| POST | `/users` | エンティティの作成、201 | +| PUT | `/users/{entity_id}` | エンティティの置換、200 または 404 | +| DELETE | `/users/{entity_id}` | エンティティの削除、空の 204 または 404 | + +ドメインのプレフィックスとタグは、クラシックレイアウトと同様に、共有の API ルーターが `include_router` を呼び出すときに設定します。 + +アプリケーションの既存の API プレフィックスはそのまま有効です (例: `/api/v1/users`)。ドメインのルートには PATCH は含まれません。 + +生成された repository は自己完結しており、CRUD 操作をロックで保護します。デフォルトのインスタンスは **このドメインとプロセスの中で** リクエスト間で共有されます。 +データは再起動で失われ、ワーカープロセス間では共有されません。永続化が必要な場合は repository を書き換えてください。repository はスターターの `db.memory`、ORM、データベース設定には依存していません。 + +テストを分離するには、新しい `UsersRepository` を `UsersService` に注入し、router の `get_users_service` という FastAPI 依存性をオーバーライドします。 +`UsersRepository.reset()` はその repository を消去し、ID を 1 から振り直します。 +`service.reset_store()` は、リクエスト間で共有されるデフォルトの repository をリセットします。 +明示的に作成した repository インスタンスは、それぞれ独立したストレージを持ちます。 + +## コマンドを再実行する + +既存のモジュールファイルは保持され、不足しているファイルが作成されます。import とルーター登録は重複しません。異なるレイアウトの同名モジュールは共存でき、import エイリアスによって Python の名前衝突を避けます。HTTP メソッドとパスの衝突はジェネレーターがチェックしません。`--layout` は既存のモジュールを移行しません。 + ## 基本的なルート追加 +次の例は `classic-layer` レイアウトを前提にしています。domain-starter プロジェクトで使う場合は、`--layout=classic-layer` を明示的に指定してください。 + ### `addroute` コマンドを使う FastAPI-fastkit の `addroute` コマンドは、新しいルートの追加を簡単にします: diff --git a/docs/ja/user-guide/cli-reference.md b/docs/ja/user-guide/cli-reference.md index f2ea175..59e7f70 100644 --- a/docs/ja/user-guide/cli-reference.md +++ b/docs/ja/user-guide/cli-reference.md @@ -337,6 +337,7 @@ $ fastkit addroute ROUTE_NAME [PROJECT_DIR] [OPTIONS] | オプション | 説明 | デフォルト | |---|---|---| +| `--layout [classic-layer\|domain]` | 生成するルートモジュールの構造。プリセットのメタデータより優先される | プリセットから自動判定 | | `--help` | コマンドのヘルプを表示 | - | #### 例 @@ -372,25 +373,43 @@ $ fastkit addroute users my-api #### 生成されるファイル -プロジェクトに次のファイルが作成されます: +`--layout=classic-layer` の場合、アプリパッケージの下に次のファイルが作成されます: - `src/api/routes/users.py` - ルートハンドラ - `src/crud/users.py` - CRUD 操作 - `src/schemas/users.py` - Pydantic スキーマ -また、新しいルーターを取り込むよう `src/api/api.py` も自動更新されます。 +`--layout=domain` の場合は、`__init__.py`、`models.py`、`schemas.py`、`repository.py`、`service.py`、`router.py` を含む `domains/users/` が作成されます。 +エンティティは `id: int` と `name: str` を持ち、動作するインメモリ repository が付属します。 + +どちらのレイアウトも既存の API ルーターを更新し、必要に応じてアプリケーションに接続します。パスは `src/app/` を含め、実際のアプリパッケージに従います。 + +オプションを省略した場合、`[tool.fastapi-fastkit].preset = "domain-starter"` なら `domain` が選ばれます。`preset` がない場合は `template` からプリセットを推定し、`fastapi-domain-starter` でも `domain` が選ばれます。その他のプリセットや、どちらのメタデータ値も持たないプロジェクトでは `classic-layer` が選ばれます。未知のプリセットは警告を出したうえで `classic-layer` にフォールバックします。明示的に選択しても、プロジェクトのプリセットは変更されません。 + +```console +$ fastkit addroute users . --layout=domain +$ fastkit addroute products . --layout=classic-layer +``` + +コマンドは確認の前に選択したレイアウトを表示します。再実行すると、既存のファイルは保持され、不足しているファイルは復元され、ルーター登録の重複は避けられます。異なるレイアウトの同名モジュールは、import エイリアスによって共存できます。ジェネレーターは HTTP メソッドとパスの衝突をチェックしません。 +移行は手動で行います。 #### 生成されるエンドポイント -完全な CRUD エンドポイントが作成されます: +ドメインレイアウトでは次のエンドポイントが作成されます (アプリの既存プレフィックスが `/api/v1` の場合の例): | メソッド | エンドポイント | 説明 | |---|---|---| -| `GET` | `/api/v1/users/` | すべてのユーザーを取得 | -| `POST` | `/api/v1/users/` | 新しいユーザーを作成 | -| `GET` | `/api/v1/users/{user_id}` | 特定のユーザーを取得 | -| `PUT` | `/api/v1/users/{user_id}` | ユーザーを更新 | -| `DELETE` | `/api/v1/users/{user_id}` | ユーザーを削除 | +| `GET` | `/api/v1/users` | エンティティの一覧、200 | +| `POST` | `/api/v1/users` | エンティティの作成、201 | +| `GET` | `/api/v1/users/{entity_id}` | エンティティの取得、200 または 404 | +| `PUT` | `/api/v1/users/{entity_id}` | エンティティの置換、200 または 404 | +| `DELETE` | `/api/v1/users/{entity_id}` | エンティティの削除、空の 204 または 404 | + +POST と PUT は 1〜120 文字の `name` を受け付けます。ID は repository が割り当て、PUT はそれを維持します。ドメインには PATCH エンドポイントは生成されません。 +ストレージはプロセスローカルで、再起動で失われ、ドメイン間で独立しています。永続化が必要な場合は repository を置き換えてください。テストでは service を通じて新しい repository を注入し、`get_users_service` をオーバーライドできます。 + +クラシックレイアウトでは、従来どおり `//` に GET/POST/PUT/PATCH/DELETE のスタブが作成され、実装はあなたに任されます。 #### コードが挿入される場所 @@ -410,7 +429,7 @@ api_router.include_router(health.router) ``` ファイルを編集するときは、これらのコメントをそのまま残してください — `addroute` は `# fastkit:imports` と -`# fastkit:routes` の直上に挿入します。アンカーを失ったプロジェクト (またはアンカーが導入される前に生成された +`# fastkit:routes` の直下に挿入します。アンカーを失ったプロジェクト (またはアンカーが導入される前に生成された プロジェクト) でも動作はします: fastkit は AST ベースの挿入にフォールバックし、import ブロックとルーター登録の 位置を自分で探します。アンカーは結果を予測可能にするためのものです。 diff --git a/docs/ko/reference/translation-status.md b/docs/ko/reference/translation-status.md index cc65d64..fbc95e5 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`) | ✅ 완료 | 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-10-11, v1.4.1 릴리스 이후. 번역된 6개 로케일을 모두 영문 원본과 같은 수준으로 맞췄습니다. v1.4.0에서 추가된 튜토리얼 3편을 번역했고, 처음 번역한 뒤 영문과 어긋난 페이지도 모두 다시 동기화했습니다. 그래도 한 릴리스 안에서는 번역이 뒤처질 수 있으므로, 번역된 페이지가 영문과 다르다면 영문을 신뢰하세요.* 이 표는 수동으로 관리됩니다. 리포지토리 루트에서 현재 상태를 다시 세고 싶다면 다음 명령을 실행하세요: +| 🇰🇷 한국어 (`ko`) | ✅ 완료 | 29 / 29 | Phase 1~3은 모두 반영되어 있습니다. 2026-10-14에 v1.5.0 영문 원본과 다시 동기화했습니다. 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-14에 v1.5.0 영문 원본과 다시 동기화했습니다. 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-14에 v1.5.0 영문 원본과 다시 동기화했습니다. 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-14에 v1.5.0 영문 원본과 다시 동기화했습니다. 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-14에 v1.5.0 영문 원본과 다시 동기화했습니다. 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-14에 v1.5.0 영문 원본과 다시 동기화했습니다. v1.4.0에서 추가된 튜토리얼 3편(`tutorial/auth-jwt.md`, `tutorial/sqlmodel.md`, `tutorial/llm-agent.md`)을 번역했고, 처음 번역한 뒤 영문과 어긋난 페이지도 다시 맞췄습니다. `docs/de/changelog.md` 는 영문 기준 `CHANGELOG.md` 를 그대로 사용합니다. | + +*스냅샷 검증 시점: 2026-10-14, v1.5.0 릴리스 이후. 번역된 6개 로케일을 모두 영문 원본과 같은 수준으로 맞췄습니다. v1.4.0에서 추가된 튜토리얼 3편을 번역했고, 처음 번역한 뒤 영문과 어긋난 페이지도 모두 다시 동기화했습니다. 그래도 한 릴리스 안에서는 번역이 뒤처질 수 있으므로, 번역된 페이지가 영문과 다르다면 영문을 신뢰하세요.* 이 표는 수동으로 관리됩니다. 리포지토리 루트에서 현재 상태를 다시 세고 싶다면 다음 명령을 실행하세요: ```console $ for loc in en ko ja zh es fr de; do diff --git a/docs/ko/tutorial/domain-starter.md b/docs/ko/tutorial/domain-starter.md index 878b7ab..95beb85 100644 --- a/docs/ko/tutorial/domain-starter.md +++ b/docs/ko/tutorial/domain-starter.md @@ -7,7 +7,7 @@ - `fastkit startdemo fastapi-domain-starter`로 프로젝트 생성 - 레이아웃에서 `core`, `db`, `domains`, `tests`가 맡는 역할 - 한 도메인이 router → service → repository → schemas → models 로 분할되는 방식 -- 새 도메인 추가의 계약 (items 폴더 복사, 라우터 등록) +- `fastkit addroute`로 새 도메인을 추가하고 테스트에서 저장소를 격리하는 방법 - 번들된 `/health` 엔드포인트와 `/api/v1/items` CRUD 가 앱에 어떻게 연결되는지 ## 사전 요구 사항 @@ -76,7 +76,7 @@ orders-api/ │ ├── schemas.py # ItemCreate, ItemRead (pydantic) │ ├── repository.py # InMemoryStore 위의 ItemRepository │ ├── service.py # ItemService + ItemNotFoundError -│ └── router.py # APIRouter(prefix="/items") +│ └── router.py # APIRouter 엔드포인트 └── tests/ ├── __init__.py ├── conftest.py # TestClient 픽스처, 스토어 리셋 @@ -138,7 +138,7 @@ class InMemoryStore(Generic[T]): # src/app/api/router.py api_router = APIRouter() api_router.include_router(health.router) -api_router.include_router(items_router.router) +api_router.include_router(items_router.router, prefix="/items", tags=["items"]) ``` ```python @@ -166,7 +166,7 @@ app.include_router(api_router, prefix=settings.API_V1_PREFIX) 런타임 레이아웃을 거울처럼 따라갑니다 — 동작을 고정할 가치가 있는 표면마다 테스트 모듈 하나. 스타터는 다음을 제공합니다: -- `conftest.py` — 테스트 사이에 items 스토어를 리셋하는 autouse 픽스처와 `TestClient(app)` 를 감싸는 `client` 픽스처. +- `conftest.py` — 테스트 사이에 items 스토어를 리셋하고 생성된 도메인에 대해 `service.reset_store()` 를 호출하는 autouse 픽스처와 `TestClient(app)` 를 감싸는 `client` 픽스처. - `test_health.py` — `GET /api/v1/health` 가 200 + `{"status": "ok"}` 를 반환하는지 검증. - `test_items.py` — items 엔드포인트의 전체 CRUD 커버리지. 알 수 없는 id 의 404 와 잘못된 페이로드의 422 도 포함. @@ -245,7 +245,7 @@ class ItemService: ```python # src/app/domains/items/router.py -router = APIRouter(prefix="/items", tags=["items"]) +router = APIRouter() def get_item_service() -> ItemService: return ItemService() @@ -258,6 +258,13 @@ def get_item(item_id: int, service: ItemService = Depends(get_item_service)) -> raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(exc)) ``` +도메인의 prefix와 tags는 공용 API 라우터에서 설정합니다: + +```python +# src/app/api/router.py +api_router.include_router(items_router.router, prefix="/items", tags=["items"]) +``` + 전체 라우터가 노출하는 것: | 메서드 | 경로 | 동작 | @@ -286,98 +293,71 @@ $ curl http://127.0.0.1:8000/api/v1/items/999 ## 5단계: 다음 도메인 추가 -스타터는 **도메인 추가가 복사-이름변경 작업**이 되도록 설계됐습니다. `items` 옆에 `users` 도메인을 만들고 싶다고 가정해 봅시다: - -### 1. `items/` 폴더 복사 +생성된 프로젝트의 루트에서 `users` 도메인을 추가합니다: ```console -$ cp -r src/app/domains/items src/app/domains/users +$ fastkit addroute users ``` -### 2. 엔티티, 스키마, 파일별 클래스 이름 다시 쓰기 - -```python -# src/app/domains/users/models.py -from dataclasses import dataclass +이 명령은 `[tool.fastapi-fastkit].preset` 값을 읽고, `preset`이 없으면 `template` 값에서 프리셋을 추론합니다. 따라서 `startdemo`, 대화형 `init`, `init --config`로 만든 domain-starter 프로젝트에서는 도메인 레이아웃이 선택됩니다. 선택된 레이아웃은 확인 단계 전에 화면에 표시됩니다. 레이아웃을 명시적으로 선택할 수도 있습니다: -@dataclass -class User: - id: int - email: str - is_active: bool = True -``` - -```python -# src/app/domains/users/schemas.py -from pydantic import BaseModel, ConfigDict, Field - -class UserCreate(BaseModel): - # 평문 ``str`` 을 쓰면 그대로 붙여 넣어도 안전합니다. 대신 pydantic - # 의 내장 이메일 검증을 사용하려면 선택 의존성 - # (``pip install 'pydantic[email]'`` — ``email-validator`` 를 끌어옴) - # 을 설치하고 ``str`` 을 ``EmailStr`` 로 바꾸세요. - email: str = Field(min_length=3, max_length=320) - is_active: bool = True - -class UserRead(BaseModel): - id: int - email: str - is_active: bool - model_config = ConfigDict(from_attributes=True) +```console +$ fastkit addroute users . --layout=domain ``` -`models.py`, `schemas.py`, `repository.py`, `service.py`, `router.py` 전반에 걸쳐 `Item → User`, `ItemNotFoundError → UserNotFoundError`, `ItemRepository → UserRepository`, `ItemService → UserService` 로 이름을 바꾸세요. 라우터의 `prefix="/items"` → `prefix="/users"` 와 `tags=["items"]` → `tags=["users"]` 도 잊지 마세요. - -repository 는 같은 `InMemoryStore` 기반 패턴을 유지할 수 있습니다 — 엔티티 타입에 대해 제네릭이기 때문입니다: +`--layout=classic-layer`를 지정하면 기존 방식의 `api/routes`, `crud`, `schemas` 구조가 생성됩니다. `classic-layered`, `minimal`, `single-module` 프로젝트와 preset 및 template 메타데이터가 없는 프로젝트에서도 이 레이아웃이 기본값입니다. 알 수 없는 프리셋이면 경고를 출력하고 `classic-layer`로 대체합니다. 이 옵션은 프로젝트의 프리셋을 변경하지 않습니다. -```python -# src/app/domains/users/repository.py -_store: InMemoryStore[User] = InMemoryStore() +### 생성되는 도메인 -class UserRepository: - def __init__(self, store: Optional[InMemoryStore[User]] = None) -> None: - self._store = store if store is not None else _store - # ... ItemRepository 와 같은 모양 ... +```text +src/app/domains/users/ +├── __init__.py +├── models.py # id와 name을 가진 Users +├── schemas.py # UsersCreate와 UsersRead +├── repository.py # UsersRepository: 타입이 지정된 인메모리 CRUD +├── service.py # UsersService와 UsersNotFoundError +└── router.py # 엔드포인트와 get_users_service 의존성 ``` -### 3. 도메인 `__init__.py` 갱신 +새 도메인은 라우트 이름에서 파생한 PascalCase 클래스 이름을 사용하므로, `users`에서 단수형 이름을 추론할 필요가 없습니다. 비즈니스 개념에 필요한 필드와 동작을 넣도록 이 파일들을 수정하세요. -items 도메인은 호출자가 `from src.app.domains.items import service` 처럼 쓸 수 있도록 자기 모듈들을 다시 export 합니다. users 도 같은 방식으로: +라우터는 `src/app/api/router.py`에 `prefix="/users"`와 `tags=["users"]`로 자동 등록됩니다. 도메인 라우터 자체는 이 설정 없이 `APIRouter()`를 사용합니다. 애플리케이션에 이미 설정된 prefix는 그대로 유지되므로, 다음 엔드포인트가 노출됩니다: -```python -# src/app/domains/users/__init__.py -from src.app.domains.users import ( # noqa: F401 - models, - repository, - router, - schemas, - service, -) -``` +| 메서드 | 엔드포인트 | 결과 | +|--------|----------|--------| +| GET | `/api/v1/users` | 엔티티 목록, 200 | +| GET | `/api/v1/users/{entity_id}` | 엔티티 조회, 200 또는 404 | +| POST | `/api/v1/users` | 엔티티 생성, 201 | +| PUT | `/api/v1/users/{entity_id}` | 엔티티 교체, 200 또는 404 | +| DELETE | `/api/v1/users/{entity_id}` | 엔티티 삭제, 빈 응답의 204 또는 404 | -### 4. 집계기에 라우터 등록 +POST와 PUT은 `{"name": "Alice"}` 형태의 본문을 받으며, `name`은 1–120자여야 합니다. repository는 1부터 시작하는 ID를 부여하고, PUT은 그 ID를 유지합니다. PATCH 엔드포인트는 생성되지 않습니다. -여기가 **`domains/users/` 바깥에서 손대야 하는 유일한 파일**입니다: +새 repository는 독립적으로 동작하며, 번들된 items 도메인이 사용하는 `db.memory` 스토어를 사용하지 않습니다. 명시적으로 생성한 repository 인스턴스는 각각 독립된 저장소를 갖고, 기본 인스턴스는 이 도메인과 프로세스 안에서 요청 간에 공유됩니다. CRUD 연산은 락으로 보호됩니다. 데이터는 재시작하면 사라지고 워커 프로세스 간에 공유되지 않으므로, 영속 저장소가 필요하면 repository를 교체하세요. -```python -# src/app/api/router.py -from src.app.api import health -from src.app.domains.items import router as items_router -from src.app.domains.users import router as users_router # ← 추가 +### 격리된 테스트 추가 -api_router = APIRouter() -api_router.include_router(health.router) -api_router.include_router(items_router.router) -api_router.include_router(users_router.router) # ← 추가 -``` +items 전용 리셋 픽스처는 새 도메인을 리셋하지 않습니다. 테스트마다 새 repository를 사용하도록 서비스 의존성을 오버라이드하세요: -서버 재시작 후 `/docs` 에서 `/api/v1/users` 가 마운트된 것을 확인할 수 있습니다. +```python +from src.app.domains.users.repository import UsersRepository +from src.app.domains.users.router import get_users_service +from src.app.domains.users.service import UsersService -### 5. 테스트 추가 +def test_create_user(client): + from src.app.main import app -`tests/test_items.py` 를 거울처럼 옮긴 `tests/test_users.py` 를 만드세요 — 동일한 클라이언트 기반 모양으로 새 엔드포인트를 호출합니다. `conftest.py` 의 autouse 스토어 리셋 픽스처가 이미 각 테스트를 격리해 줍니다. + service = UsersService(repository=UsersRepository()) + app.dependency_overrides[get_users_service] = lambda: service + try: + response = client.post("/api/v1/users", json={"name": "Alice"}) + assert response.status_code == 201 + assert response.json() == {"id": 1, "name": "Alice"} + finally: + app.dependency_overrides.pop(get_users_service, None) +``` -`InMemoryStore` 를 쓰는 두 번째 도메인을 추가한다면, 그 스토어도 리셋하도록 픽스처를 확장하거나, 도메인별로 픽스처 하나씩 두세요. +`addroute`를 다시 실행하면 기존 도메인 파일은 보존되고 누락된 파일만 생성되며, import와 라우터 등록은 중복되지 않습니다. 이름이 같은 classic 모듈도 import 별칭을 통해 함께 존재할 수 있습니다. 생성된 패키지는 자기 모듈들을 다시 export하고, repository는 데이터를 비우고 ID를 처음부터 다시 매기는 `reset()`을 제공합니다. 스타터의 autouse 픽스처는 생성된 도메인에 대해 `service.reset_store()`를 호출합니다. 이전에 만든 프로젝트라면 기존 리셋 픽스처에 이 호출을 추가하거나, 의존성 오버라이드로 격리된 repository를 사용하세요. 생성기는 겹치는 엔드포인트를 감지하지 않으므로, 라우터를 조합할 때 HTTP 경로와 메서드를 직접 확인하세요. ## 6단계: 다음 행선지 @@ -389,4 +369,4 @@ api_router.include_router(users_router.router) # ← 추가 - **생성**: `fastkit startdemo fastapi-domain-starter` → `bash scripts/run-server.sh` → `/docs` 에서 문서. - **레이아웃**: 설정용 `core/`, 영속성 추상화용 `db/`, 비즈니스 슬라이스용 `domains//`, 단일 집계 지점인 `api/router.py`, 런타임 모듈을 거울처럼 따라가는 `tests/`. -- **도메인 추가**: `items/` 복사 → 엔티티 / 스키마 / 클래스 이름 변경 → `__init__.py` 의 재export 갱신 → `src/app/api/router.py` 에 라우터 등록 → 테스트 모듈 추가. `main.py` 수정은 없습니다. +- **도메인 추가**: `fastkit addroute users`를 실행하고, 생성된 `Users*` CRUD 스캐폴드를 수정한 뒤, 격리된 repository로 테스트를 추가합니다. 라우터는 자동으로 등록됩니다. diff --git a/docs/ko/user-guide/adding-routes.md b/docs/ko/user-guide/adding-routes.md index b2e398c..5b113ab 100644 --- a/docs/ko/user-guide/adding-routes.md +++ b/docs/ko/user-guide/adding-routes.md @@ -2,8 +2,64 @@ 기존 FastAPI 프로젝트에 새 API 라우트를 추가하는 방법을 안내합니다. +## 라우트 레이아웃 선택 + +`addroute`는 `--layout=classic-layer` 또는 `--layout=domain`을 받습니다. 옵션을 생략하면 `[tool.fastapi-fastkit].preset` 값을 읽습니다. `domain-starter`이면 `domain`을 선택하고, `classic-layered`, `minimal`, `single-module`이면 `classic-layer`를 선택합니다. `preset`이 없으면 `template` 값에서 프리셋을 추론하므로, `startdemo`나 대화형 `init`으로 만든 `fastapi-domain-starter` 프로젝트에서도 `domain`이 선택됩니다. 두 값이 모두 없으면 `classic-layer`를 선택합니다. 알 수 없는 프리셋이면 경고를 출력하고 `classic-layer`로 대체합니다. 옵션을 명시하면 프로젝트 메타데이터를 변경하지 않고 이 선택을 덮어씁니다. + +```console +$ cd my-api +$ fastkit addroute users . --layout=domain +$ fastkit addroute products . --layout=classic-layer +``` + +선택된 레이아웃은 확인 단계 전에 화면에 표시됩니다. 파일은 애플리케이션의 실제 패키지(`src/` 또는 `src/app/`) 아래에 배치되며, 기록된 진입점이 있으면 그 진입점을 기준으로 삼습니다. + +## 도메인 CRUD 생성 + +`domain-starter` 프로젝트에서는 다음 명령만 실행하면 됩니다: + +```console +$ fastkit addroute users +``` + +이 명령은 다음 파일을 생성하고 도메인 라우터를 자동으로 등록합니다: + +```text +src/app/domains/users/ +├── __init__.py +├── models.py # Users(id: int, name: str) +├── schemas.py # POST/PUT용 UsersCreate, 응답용 UsersRead +├── repository.py # 타입이 지정된 CRUD 연산과 인메모리 저장소 +├── service.py # 비즈니스 로직과 UsersNotFoundError +└── router.py # HTTP 엔드포인트와 오버라이드 가능한 서비스 의존성 +``` + +각 도메인 안에서 클래스 이름은 `Users`, `UsersCreate`, `UsersRead`, `UsersRepository`, `UsersService`가 됩니다. 클래스 이름은 단수형으로 바꾸지 않고 PascalCase를 사용합니다(`health_checks`는 `HealthChecks`가 됩니다). repository는 1부터 시작하는 ID를 부여합니다. POST와 PUT에는 1자 이상 120자 이하의 `name`이 필요하며, PUT은 기존 ID를 유지합니다. + +| 메서드 | 도메인 경로 | 결과 | +|--------|-------------|--------| +| GET | `/users` | 엔티티 목록, 200 | +| GET | `/users/{entity_id}` | 엔티티 조회, 200 또는 404 | +| POST | `/users` | 엔티티 생성, 201 | +| PUT | `/users/{entity_id}` | 엔티티 교체, 200 또는 404 | +| DELETE | `/users/{entity_id}` | 엔티티 삭제, 빈 응답의 204 또는 404 | + +classic 레이아웃과 마찬가지로, 도메인의 prefix와 tags는 공용 API 라우터가 `include_router`를 호출할 때 설정됩니다. + +애플리케이션에 이미 설정된 API prefix는 그대로 적용됩니다(예: `/api/v1/users`). 도메인 라우트에는 PATCH가 포함되지 않습니다. + +생성된 repository는 독립적으로 동작하며 CRUD 연산에 락을 겁니다. 기본 인스턴스는 **이 도메인과 프로세스 안에서** 요청 간에 공유됩니다. 데이터는 재시작하면 사라지고 워커 프로세스 간에 공유되지 않습니다. 영속성이 필요하면 repository를 수정하세요. 이 repository는 스타터의 `db.memory`, ORM, 데이터베이스 설정에 의존하지 않습니다. + +격리된 테스트를 작성하려면 새 `UsersRepository`를 `UsersService`에 주입한 뒤, 라우터의 `get_users_service` FastAPI 의존성을 오버라이드하세요. `UsersRepository.reset()`은 해당 repository를 비우고 ID를 다시 1부터 매깁니다. `service.reset_store()`는 요청 간에 공유되는 기본 repository를 리셋합니다. 명시적으로 생성한 repository 인스턴스는 각각 독립된 저장소를 갖습니다. + +## 명령을 다시 실행할 때 + +기존 모듈 파일은 보존되고 누락된 파일만 생성됩니다. import와 라우터 등록은 중복되지 않습니다. 레이아웃이 다른 같은 이름의 모듈도 함께 존재할 수 있으며, import 별칭으로 Python 이름 충돌을 피합니다. 생성기는 HTTP 메서드/경로 충돌을 검사하지 않습니다. `--layout`은 기존 모듈을 마이그레이션하지 않습니다. + ## 기본 라우트 추가 +다음 예시는 `classic-layer` 레이아웃을 기준으로 설명합니다. domain-starter 프로젝트에서 이 레이아웃을 쓰려면 `--layout=classic-layer`를 명시적으로 전달하세요. + ### `addroute` 명령 사용 FastAPI-fastkit의 `addroute` 명령을 쓰면 새 라우트를 간편하게 추가할 수 있습니다: diff --git a/docs/ko/user-guide/cli-reference.md b/docs/ko/user-guide/cli-reference.md index 9e67325..3d3a29f 100644 --- a/docs/ko/user-guide/cli-reference.md +++ b/docs/ko/user-guide/cli-reference.md @@ -348,6 +348,7 @@ $ fastkit addroute ROUTE_NAME [PROJECT_DIR] [OPTIONS] | 옵션 | 설명 | 기본값 | |--------|-------------|---------| +| `--layout [classic-layer\|domain]` | 생성할 라우트 모듈의 구조. 프리셋 메타데이터보다 우선 적용 | 프리셋에 따라 자동 결정 | | `--help` | 명령 도움말 표시 | - | #### 예시 @@ -383,25 +384,40 @@ $ fastkit addroute users my-api #### 생성되는 파일 -프로젝트에 다음 파일들을 생성합니다: +`--layout=classic-layer`를 지정하면 앱 패키지 아래에 다음 파일들을 생성합니다: - `src/api/routes/users.py` - 라우트 핸들러 - `src/crud/users.py` - CRUD 작업 - `src/schemas/users.py` - Pydantic 스키마 -또한 `src/api/api.py` 가 갱신되어 새 라우터가 포함됩니다. +`--layout=domain`을 지정하면 `__init__.py`, `models.py`, `schemas.py`, `repository.py`, `service.py`, `router.py`가 들어 있는 `domains/users/`를 생성합니다. 엔티티는 `id: int`와 `name: str`을 가지며, 실제로 동작하는 인메모리 repository가 함께 생성됩니다. + +두 레이아웃 모두 기존 API 라우터를 갱신하고, 필요하면 그 라우터를 애플리케이션에 연결합니다. 경로는 `src/app/`을 포함해 실제 앱 패키지를 따릅니다. + +옵션을 생략하면 `[tool.fastapi-fastkit].preset = "domain-starter"`일 때 `domain`이 선택됩니다. `preset`이 없으면 `template` 값에서 프리셋을 추론하며, `fastapi-domain-starter`도 `domain`을 선택합니다. 그 밖의 프리셋과 두 메타데이터 값이 모두 없는 프로젝트에서는 `classic-layer`가 선택됩니다. 알 수 없는 프리셋이면 경고를 출력하고 `classic-layer`로 대체합니다. 명시적으로 선택해도 프로젝트의 프리셋은 변경되지 않습니다. + +```console +$ fastkit addroute users . --layout=domain +$ fastkit addroute products . --layout=classic-layer +``` + +이 명령은 확인 단계 전에 선택된 레이아웃을 표시합니다. 다시 실행하면 기존 파일은 보존하고, 누락된 파일은 복원하며, 라우터 등록은 중복하지 않습니다. 레이아웃이 다른 같은 이름의 모듈도 import 별칭을 통해 함께 존재할 수 있습니다. 생성기는 HTTP 메서드/경로 충돌을 검사하지 않습니다. 마이그레이션은 직접 수행해야 합니다. #### 생성되는 엔드포인트 -전체 CRUD 엔드포인트가 만들어집니다: +도메인 레이아웃은 다음 엔드포인트를 생성합니다(앱에 이미 설정된 prefix가 `/api/v1`이라고 가정합니다): | 메서드 | 엔드포인트 | 설명 | |--------|----------|-------------| -| `GET` | `/api/v1/users/` | 모든 사용자 조회 | -| `POST` | `/api/v1/users/` | 새 사용자 생성 | -| `GET` | `/api/v1/users/{user_id}` | 특정 사용자 조회 | -| `PUT` | `/api/v1/users/{user_id}` | 사용자 갱신 | -| `DELETE` | `/api/v1/users/{user_id}` | 사용자 삭제 | +| `GET` | `/api/v1/users` | 엔티티 목록, 200 | +| `POST` | `/api/v1/users` | 엔티티 생성, 201 | +| `GET` | `/api/v1/users/{entity_id}` | 엔티티 조회, 200 또는 404 | +| `PUT` | `/api/v1/users/{entity_id}` | 엔티티 교체, 200 또는 404 | +| `DELETE` | `/api/v1/users/{entity_id}` | 엔티티 삭제, 빈 응답의 204 또는 404 | + +POST와 PUT은 1–120자의 `name`을 받습니다. ID는 repository가 부여하고, PUT은 그 ID를 유지합니다. 도메인 레이아웃에서는 PATCH 엔드포인트가 생성되지 않습니다. 저장소는 프로세스 로컬이고, 재시작하면 사라지며, 도메인마다 독립적입니다. 영속성이 필요하면 repository를 교체하세요. 테스트에서는 서비스에 새 repository를 주입하고 `get_users_service`를 오버라이드할 수 있습니다. + +classic 레이아웃은 직접 구현할 수 있도록 `//` 경로에 기존 GET/POST/PUT/PATCH/DELETE 스텁을 그대로 생성합니다. #### 코드가 삽입되는 위치 @@ -422,7 +438,7 @@ api_router.include_router(health.router) ``` 이 파일을 직접 수정할 때도 주석은 그대로 두세요 — `addroute` 는 -`# fastkit:imports` 와 `# fastkit:routes` 바로 위에 코드를 넣습니다. 주석을 +`# fastkit:imports` 와 `# fastkit:routes` 바로 아래에 코드를 넣습니다. 주석을 잃어버린 프로젝트(또는 앵커가 생기기 전에 만들어진 프로젝트)도 동작합니다. fastkit 이 AST 기반 삽입으로 되돌아가 import 블록과 라우터 등록부를 스스로 찾습니다. 앵커는 그 결과를 예측 가능하게 만들어 줄 뿐입니다. diff --git a/docs/zh/reference/translation-status.md b/docs/zh/reference/translation-status.md index 3088092..ede83d3 100644 --- a/docs/zh/reference/translation-status.md +++ b/docs/zh/reference/translation-status.md @@ -19,14 +19,14 @@ FastAPI-fastkit 以多种语言发布文档,但这些翻译**并不总是完全 | 语言 | 状态 | Markdown 页面数 | 备注 | |---|---|---:|---| | 🇬🇧 英文(`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-10-11,v1.4.1 发布之后。全部六个翻译语言都已与英文源达到一致:v1.4.0 新增的三篇教程已经翻译,所有自最初翻译以来出现偏差的页面也都已重新同步。在一个版本周期内,翻译仍可能暂时落后 —— 如果某个翻译页面与英文不一致,请以英文为准。* 此表通过人工维护;如需在仓库根目录重新统计当前状态,请运行: +| 🇰🇷 韩文(`ko`) | ✅ 完成 | 29 / 29 | Phase 1-3 已完成。已于 2026-10-14 与 v1.5.0 的英文源重新同步: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-14 与 v1.5.0 的英文源重新同步: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-14 与 v1.5.0 的英文源重新同步: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-14 与 v1.5.0 的英文源重新同步: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-14 与 v1.5.0 的英文源重新同步: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-14 与 v1.5.0 的英文源重新同步:v1.4.0 新增的三篇教程(`tutorial/auth-jwt.md`、`tutorial/sqlmodel.md`、`tutorial/llm-agent.md`)已经翻译,自最初翻译以来与英文产生偏差的页面也已重新对齐。`docs/de/changelog.md` 刻意复用规范的英文 `CHANGELOG.md`。 | + +*快照验证时间:2026-10-14,v1.5.0 发布之后。全部六个翻译语言都已与英文源达到一致:v1.4.0 新增的三篇教程已经翻译,所有自最初翻译以来出现偏差的页面也都已重新同步。在一个版本周期内,翻译仍可能暂时落后 —— 如果某个翻译页面与英文不一致,请以英文为准。* 此表通过人工维护;如需在仓库根目录重新统计当前状态,请运行: ```console $ for loc in en ko ja zh es fr de; do diff --git a/docs/zh/tutorial/domain-starter.md b/docs/zh/tutorial/domain-starter.md index 8610fb6..e7d5b30 100644 --- a/docs/zh/tutorial/domain-starter.md +++ b/docs/zh/tutorial/domain-starter.md @@ -7,7 +7,7 @@ - 使用 `fastkit startdemo fastapi-domain-starter` 生成项目 - 布局中 `core`、`db`、`domains`、`tests` 各自的角色 - 一个领域如何拆分为 router → service → repository → schemas → models -- 添加新领域的契约(复制 items 文件夹,注册路由) +- 使用 `fastkit addroute` 添加新领域,并在测试中隔离其存储 - 自带的 `/health` 端点与 `/api/v1/items` CRUD 如何接入应用 ## 前置条件 @@ -76,7 +76,7 @@ orders-api/ │ ├── schemas.py # ItemCreate, ItemRead (pydantic) │ ├── repository.py # ItemRepository over InMemoryStore │ ├── service.py # ItemService + ItemNotFoundError -│ └── router.py # APIRouter(prefix="/items") +│ └── router.py # APIRouter endpoints └── tests/ ├── __init__.py ├── conftest.py # TestClient fixture, store reset @@ -138,7 +138,7 @@ class InMemoryStore(Generic[T]): # src/app/api/router.py api_router = APIRouter() api_router.include_router(health.router) -api_router.include_router(items_router.router) +api_router.include_router(items_router.router, prefix="/items", tags=["items"]) ``` ```python @@ -166,7 +166,7 @@ app.include_router(api_router, prefix=settings.API_V1_PREFIX) 镜像运行时布局 —— 对每个值得固定行为的对外面都对应一个测试模块。starter 自带: -- `conftest.py` —— autouse fixture,在测试之间重置 items 存储;另外一个 `client` fixture 封装了 `TestClient(app)`。 +- `conftest.py` —— autouse fixture,在测试之间重置 items 存储,并对生成的领域调用 `service.reset_store()`;另外一个 `client` fixture 封装了 `TestClient(app)`。 - `test_health.py` —— 验证 `GET /api/v1/health` 返回 200 + `{"status": "ok"}`。 - `test_items.py` —— 对 items 端点做完整 CRUD 覆盖,包括对未知 id 返回 404、对无效负载返回 422。 @@ -245,7 +245,7 @@ Router 是唯一了解 HTTP 的那一层。注意它通过 FastAPI 的 `Depends( ```python # src/app/domains/items/router.py -router = APIRouter(prefix="/items", tags=["items"]) +router = APIRouter() def get_item_service() -> ItemService: return ItemService() @@ -258,6 +258,13 @@ def get_item(item_id: int, service: ItemService = Depends(get_item_service)) -> raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(exc)) ``` +共享的 API 路由器负责设置领域的前缀与标签: + +```python +# src/app/api/router.py +api_router.include_router(items_router.router, prefix="/items", tags=["items"]) +``` + 完整的 router 暴露: | 方法 | 路径 | 作用 | @@ -286,98 +293,71 @@ $ curl http://127.0.0.1:8000/api/v1/items/999 ## 第 5 步:添加下一个领域 -starter 的设计目标是**让添加领域变成一次复制 + 改名操作**。假设您想在 `items` 之外加一个 `users` 领域: - -### 1. 复制 `items/` 文件夹 +在生成项目的根目录下,添加一个 `users` 领域: ```console -$ cp -r src/app/domains/items src/app/domains/users +$ fastkit addroute users ``` -### 2. 重写实体、模式以及各文件中的类名 - -```python -# src/app/domains/users/models.py -from dataclasses import dataclass +该命令读取 `[tool.fastapi-fastkit].preset`;当 `preset` 不存在时,会根据 `template` 推断。因此,通过 `startdemo`、交互式 `init` 或 `init --config` 创建的项目会为 domain-starter 选择 domain 布局。所选布局会在确认之前显示出来。您也可以显式指定: -@dataclass -class User: - id: int - email: str - is_active: bool = True -``` - -```python -# src/app/domains/users/schemas.py -from pydantic import BaseModel, ConfigDict, Field - -class UserCreate(BaseModel): - # Plain ``str`` keeps the snippet drop-in safe. To use pydantic's - # built-in email validation instead, install the optional dependency - # (``pip install 'pydantic[email]'`` — pulls in ``email-validator``) - # and switch ``str`` to ``EmailStr``. - email: str = Field(min_length=3, max_length=320) - is_active: bool = True - -class UserRead(BaseModel): - id: int - email: str - is_active: bool - model_config = ConfigDict(from_attributes=True) +```console +$ fastkit addroute users . --layout=domain ``` -在 `models.py`、`schemas.py`、`repository.py`、`service.py` 与 `router.py` 中把 `Item → User`、`ItemNotFoundError → UserNotFoundError`、`ItemRepository → UserRepository`、`ItemService → UserService` 全部改名。别忘了 router 中的 `prefix="/items"` → `prefix="/users"`,以及 `tags=["items"]` → `tags=["users"]`。 - -Repository 仍可保留同样的 `InMemoryStore` 模式 —— 它本身就是基于实体类型的泛型: +`--layout=classic-layer` 会生成传统的 `api/routes`、`crud` 与 `schemas` 结构。对于 `classic-layered`、`minimal`、`single-module` 以及没有 preset 或 template 元数据的项目,这也是默认值。未知的 preset 会回退到 `classic-layer` 并给出警告。该选项不会改变项目的 preset。 -```python -# src/app/domains/users/repository.py -_store: InMemoryStore[User] = InMemoryStore() +### 生成的领域 -class UserRepository: - def __init__(self, store: Optional[InMemoryStore[User]] = None) -> None: - self._store = store if store is not None else _store - # ... same shape as ItemRepository ... +```text +src/app/domains/users/ +├── __init__.py +├── models.py # Users with id and name +├── schemas.py # UsersCreate and UsersRead +├── repository.py # UsersRepository: typed, in-memory CRUD +├── service.py # UsersService and UsersNotFoundError +└── router.py # endpoints and get_users_service dependency ``` -### 3. 更新领域的 `__init__.py` +新领域使用由路由名称派生的 PascalCase 类名,因此无需从 `users` 推断单数形式。请根据您的业务概念所需的字段与行为来定制这些文件。 -items 领域会重新导出其模块,以便调用方可以写 `from src.app.domains.items import service`。对 users 也照样做: +路由器会自动注册到 `src/app/api/router.py`,并带有 `prefix="/users"` 与 `tags=["users"]`。领域路由器本身使用不带这些设置的 `APIRouter()`。应用已有的前缀保持不变,暴露以下端点: -```python -# src/app/domains/users/__init__.py -from src.app.domains.users import ( # noqa: F401 - models, - repository, - router, - schemas, - service, -) -``` +| 方法 | 端点 | 结果 | +|--------|----------|--------| +| GET | `/api/v1/users` | 列出实体,200 | +| GET | `/api/v1/users/{entity_id}` | 读取实体,200 或 404 | +| POST | `/api/v1/users` | 创建实体,201 | +| PUT | `/api/v1/users/{entity_id}` | 替换实体,200 或 404 | +| DELETE | `/api/v1/users/{entity_id}` | 删除实体,空的 204 或 404 | -### 4. 在聚合器中注册路由 +POST 与 PUT 接受 `{"name": "Alice"}`;`name` 的长度必须为 1–120 个字符。Repository 从 1 开始分配 ID,PUT 会保留该 ID。不会生成 PATCH 端点。 -这是 **`domains/users/` 之外您唯一需要修改的文件**: +新的 repository 是自包含的;它不使用自带 items 领域的 `db.memory` 存储。每个显式创建的 repository 实例都有独立的存储,而默认实例在该领域与该进程内的请求之间共享。CRUD 操作受锁保护。数据会在重启后丢失,且不会在 worker 进程之间共享;需要持久化存储时,请替换该 repository。 -```python -# src/app/api/router.py -from src.app.api import health -from src.app.domains.items import router as items_router -from src.app.domains.users import router as users_router # ← add +### 添加隔离的测试 -api_router = APIRouter() -api_router.include_router(health.router) -api_router.include_router(items_router.router) -api_router.include_router(users_router.router) # ← add -``` +items 专用的重置 fixture 不会重置新领域。请为每个测试用一个全新的 repository 覆盖其 service 依赖: -重启服务器后,您会在 `/docs` 中看到挂载好的 `/api/v1/users`。 +```python +from src.app.domains.users.repository import UsersRepository +from src.app.domains.users.router import get_users_service +from src.app.domains.users.service import UsersService -### 5. 添加测试 +def test_create_user(client): + from src.app.main import app -把 `tests/test_items.py` 镜像成 `tests/test_users.py` —— 同样基于 client 的结构,只是请求新端点。`conftest.py` 中 autouse 的存储重置 fixture 已经能保证每个测试的隔离。 + service = UsersService(repository=UsersRepository()) + app.dependency_overrides[get_users_service] = lambda: service + try: + response = client.post("/api/v1/users", json={"name": "Alice"}) + assert response.status_code == 201 + assert response.json() == {"id": 1, "name": "Alice"} + finally: + app.dependency_overrides.pop(get_users_service, None) +``` -如果第二个领域也使用 `InMemoryStore`,可以扩展该 fixture 让它也重置该存储,或者每个领域各保留一个 fixture。 +重新运行 `addroute` 会保留已有的领域文件、创建缺失的文件,并避免重复的导入与注册。同名的 classic 模块可以通过导入别名共存。生成的包会重新导出其模块,repository 提供 `reset()` 用于清空数据并重新从头分配 ID。starter 的 autouse fixture 会对生成的领域调用 `service.reset_store()`。在较旧的项目中,请把这一调用加入已有的重置 fixture,或通过依赖覆盖使用隔离的 repository。组合路由器时,请自行检查 HTTP 路径与方法;生成器不会检测重叠的端点。 ## 第 6 步:接下来去哪里 @@ -389,4 +369,4 @@ api_router.include_router(users_router.router) # ← add - **生成**:`fastkit startdemo fastapi-domain-starter` → `bash scripts/run-server.sh` → 在 `/docs` 查看文档。 - **布局**:`core/` 放配置,`db/` 放持久化抽象,`domains//` 放业务切片,`api/router.py` 作为唯一聚合点,`tests/` 镜像运行时模块。 -- **添加领域**:复制 `items/`,改实体 / 模式 / 类名,更新 `__init__.py` 的重新导出,在 `src/app/api/router.py` 注册路由,新增测试模块。`main.py` 完全不需要改动。 +- **添加领域**:运行 `fastkit addroute users`,定制生成的 `Users*` CRUD 脚手架,并使用隔离的 repository 添加测试。路由器会自动注册。 diff --git a/docs/zh/user-guide/adding-routes.md b/docs/zh/user-guide/adding-routes.md index e54880c..91e174c 100644 --- a/docs/zh/user-guide/adding-routes.md +++ b/docs/zh/user-guide/adding-routes.md @@ -2,8 +2,64 @@ 学习如何向已有的 FastAPI 项目添加新的 API 路由。 +## 选择路由布局 + +`addroute` 接受 `--layout=classic-layer` 或 `--layout=domain`。省略时,它会读取 `[tool.fastapi-fastkit].preset`:`domain-starter` 选择 `domain`;`classic-layered`、`minimal` 与 `single-module` 选择 `classic-layer`。如果缺少 `preset`,则根据 `template` 推断 preset,因此通过 `startdemo` 或交互式 `init` 创建的 `fastapi-domain-starter` 项目也会选择 `domain`。两者都不存在时,选择 `classic-layer`。未知的 preset 会回退到 `classic-layer` 并给出警告。显式指定的选项会覆盖上述选择,但不会改变项目的元数据。 + +```console +$ cd my-api +$ fastkit addroute users . --layout=domain +$ fastkit addroute products . --layout=classic-layer +``` + +所选布局会在确认之前显示出来。文件会放在应用实际所在的包(`src/` 或 `src/app/`)下,若项目记录了入口文件则以其为准。 + +## 生成领域 CRUD + +对于 `domain-starter` 项目,只需运行: + +```console +$ fastkit addroute users +``` + +这会创建以下文件,并自动注册该领域的路由器: + +```text +src/app/domains/users/ +├── __init__.py +├── models.py # Users(id: int, name: str) +├── schemas.py # UsersCreate for POST/PUT, UsersRead for responses +├── repository.py # typed CRUD operations and in-memory storage +├── service.py # business logic and UsersNotFoundError +└── router.py # HTTP endpoints and overridable service dependency +``` + +每个领域中的类分别命名为 `Users`、`UsersCreate`、`UsersRead`、`UsersRepository` 与 `UsersService`;类名使用 PascalCase(`health_checks` 会变成 `HealthChecks`),不做单数化处理。Repository 从 1 开始分配 ID。POST 与 PUT 要求 `name` 的长度在 1 到 120 个字符之间;PUT 会保留已有的 ID。 + +| 方法 | 领域路径 | 结果 | +|--------|-------------|--------| +| GET | `/users` | 列出实体,200 | +| GET | `/users/{entity_id}` | 读取实体,200 或 404 | +| POST | `/users` | 创建实体,201 | +| PUT | `/users/{entity_id}` | 替换实体,200 或 404 | +| DELETE | `/users/{entity_id}` | 删除实体,空的 204 或 404 | + +与 classic 布局一样,领域的前缀与标签由共享的 API 路由器在调用 `include_router` 时设置。 + +应用已有的 API 前缀仍然生效(例如 `/api/v1/users`)。领域路由不包含 PATCH。 + +生成的 repository 是自包含的,并对 CRUD 操作加锁。其默认实例在**该领域与该进程内**的请求之间共享。数据会在重启后丢失,且不会在 worker 进程之间共享。如需持久化,请改造该 repository;它不依赖 starter 的 `db.memory`、ORM 或数据库配置。 + +若要编写隔离的测试,请把一个全新的 `UsersRepository` 注入 `UsersService`,然后覆盖路由器的 `get_users_service` FastAPI 依赖。`UsersRepository.reset()` 会清空该 repository,并让 ID 重新从 1 开始。`service.reset_store()` 会重置在请求之间共享的默认 repository。显式创建的 repository 实例拥有独立的存储。 + +## 重新运行命令 + +已有的模块文件会被保留;缺失的文件会被创建。import 与路由器注册不会重复。不同布局中的同名模块可以共存;导入别名可以避免 Python 名称冲突。生成器不会检查 HTTP 方法/路径冲突。`--layout` 不会迁移已有的模块。 + ## 基本的路由添加 +以下示例描述的是 `classic-layer` 布局。在 domain-starter 项目中使用时,请显式传入 `--layout=classic-layer`。 + ### 使用 `addroute` 命令 FastAPI-fastkit 的 `addroute` 命令让添加新路由变得很简单: diff --git a/docs/zh/user-guide/cli-reference.md b/docs/zh/user-guide/cli-reference.md index 5b1f563..0b587d7 100644 --- a/docs/zh/user-guide/cli-reference.md +++ b/docs/zh/user-guide/cli-reference.md @@ -311,6 +311,7 @@ $ fastkit addroute ROUTE_NAME [PROJECT_DIR] [OPTIONS] | 选项 | 描述 | 默认值 | |--------|-------------|---------| +| `--layout [classic-layer\|domain]` | 生成的路由模块的结构;会覆盖 preset 元数据 | 根据 preset 自动选择 | | `--help` | 显示命令帮助 | - | #### 示例 @@ -346,25 +347,40 @@ $ fastkit addroute users my-api #### 生成的文件 -会在项目中生成以下文件: +使用 `--layout=classic-layer` 时,会在应用包下生成以下文件: - `src/api/routes/users.py` —— 路由处理器 - `src/crud/users.py` —— CRUD 操作 - `src/schemas/users.py` —— Pydantic 模式 -同时会更新 `src/api/api.py`,把新路由器纳入其中。 +使用 `--layout=domain` 时,会创建 `domains/users/`,其中包含 `__init__.py`、`models.py`、`schemas.py`、`repository.py`、`service.py` 与 `router.py`。实体包含 `id: int` 与 `name: str`,并配有一个可用的内存 repository。 + +两种布局都会更新已有的 API 路由器,并在必要时把它连接到应用。路径遵循应用实际所在的包,包括 `src/app/`。 + +不指定该选项时,`[tool.fastapi-fastkit].preset = "domain-starter"` 会选择 `domain`。缺少 `preset` 时,会根据 `template` 推断 preset;`fastapi-domain-starter` 同样选择 `domain`。其他 preset 以及两种元数据都没有的项目会选择 `classic-layer`。未知的 preset 会回退到 `classic-layer` 并给出警告。显式选择不会改变项目的 preset。 + +```console +$ fastkit addroute users . --layout=domain +$ fastkit addroute products . --layout=classic-layer +``` + +该命令会在确认之前显示所选布局。重新运行时会保留已有文件、恢复缺失的文件,并避免重复注册路由器。不同布局的同名模块可以通过导入别名共存。生成器不会检查 HTTP 方法/路径冲突。迁移需要手动完成。 #### 生成的端点 -会创建完整的一组 CRUD 端点: +domain 布局会创建以下端点(以 `/api/v1` 作为应用已有的前缀展示): | 方法 | 端点 | 描述 | |--------|----------|-------------| -| `GET` | `/api/v1/users/` | 获取所有用户 | -| `POST` | `/api/v1/users/` | 创建新用户 | -| `GET` | `/api/v1/users/{user_id}` | 获取指定用户 | -| `PUT` | `/api/v1/users/{user_id}` | 更新用户 | -| `DELETE` | `/api/v1/users/{user_id}` | 删除用户 | +| `GET` | `/api/v1/users` | 列出实体,200 | +| `POST` | `/api/v1/users` | 创建实体,201 | +| `GET` | `/api/v1/users/{entity_id}` | 读取实体,200 或 404 | +| `PUT` | `/api/v1/users/{entity_id}` | 替换实体,200 或 404 | +| `DELETE` | `/api/v1/users/{entity_id}` | 删除实体,空的 204 或 404 | + +POST 与 PUT 接受长度为 1–120 个字符的 `name`。ID 由 repository 分配,PUT 会保留它。domain 布局不会生成 PATCH 端点。存储位于进程本地,重启后会丢失,且各领域之间相互独立;如需持久化,请替换该 repository。测试可以通过 service 注入一个全新的 repository,并覆盖 `get_users_service`。 + +classic 布局保留其原有的 GET/POST/PUT/PATCH/DELETE 存根,位于 `//`,供您自行实现。 #### 代码插入的位置 @@ -382,7 +398,7 @@ api_router.include_router(health.router) # fastkit:routes ``` -编辑该文件时请保留这些注释 —— `addroute` 会直接插入到 `# fastkit:imports` 和 `# fastkit:routes` 的上方。丢失了这些锚点(或在锚点出现之前生成)的项目仍然可以工作:fastkit 会退回到基于 AST 的插入方式,自行找到 import 块和路由注册位置。锚点只是让结果变得可预测。 +编辑该文件时请保留这些注释 —— `addroute` 会直接插入到 `# fastkit:imports` 和 `# fastkit:routes` 的下方。丢失了这些锚点(或在锚点出现之前生成)的项目仍然可以工作:fastkit 会退回到基于 AST 的插入方式,自行找到 import 块和路由注册位置。锚点只是让结果变得可预测。 `addroute` 还会读取项目的 `[tool.fastapi-fastkit]` 块来定位入口文件,因此即使在仅凭磁盘扫描会判断错误的布局上也能正常工作。 diff --git a/pdm.lock b/pdm.lock index c4501cb..14332ab 100644 --- a/pdm.lock +++ b/pdm.lock @@ -5,7 +5,7 @@ groups = ["default", "dev", "docs", "translation"] strategy = ["inherit_metadata"] lock_version = "4.5.0" -content_hash = "sha256:8f0df0c0e00dc1ca95ccb9f01c7e8faa87a40da560806c6bb5e767c66c71c27f" +content_hash = "sha256:110de45e63dfa0456adaccb1332e234b1e0e96bf900bca03dd72af4b9777275d" [[metadata.targets]] requires_python = ">=3.12" @@ -1506,7 +1506,7 @@ files = [ [[package]] name = "virtualenv" -version = "21.7.9" +version = "21.7.16" requires_python = ">=3.9" summary = "Virtual Python Environment builder" groups = ["dev"] @@ -1519,8 +1519,8 @@ dependencies = [ "typing-extensions>=4.13.2; python_version < \"3.11\"", ] files = [ - {file = "virtualenv-21.7.9-py3-none-any.whl", hash = "sha256:ba3b0bb41063c848d84d76a9fe3fb7711aaa0f2fe78708e8f3cd9714770e4eac"}, - {file = "virtualenv-21.7.9.tar.gz", hash = "sha256:a7e42d81d779dec8afd7dc4be71640fb959ea861bccfa5980cb4ad9f92e30675"}, + {file = "virtualenv-21.7.16-py3-none-any.whl", hash = "sha256:752fd5d5352502fbd794422983eed65a249fc6f4685c5e2122fd6052e6477a11"}, + {file = "virtualenv-21.7.16.tar.gz", hash = "sha256:d3fcf6954a07121c2b61efcb96419e2f40b3718d558998f13a7314e32cc42723"}, ] [[package]] diff --git a/pyproject.toml b/pyproject.toml index 9ac36b1..32fb8d7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -26,6 +26,9 @@ dev = [ # pytest < 9.0.3 — tmpdir handling (GHSA) # black < 26.3.1 — arbitrary file writes via cache file name # pygments < 2.20.0 — ReDoS in GUID regex (transitive via rich) + # virtualenv < 21.7.13 — activation-script command injection, unverified + # seed wheels, pyvenv.cfg injection (GHSA-p58f-9548-mpm2 + # and three related advisories; transitive via pre-commit) "pytest>=9.0.3", "pytest-cov>=6.2.1", "black>=26.3.1", @@ -33,6 +36,7 @@ dev = [ "mypy>=1.12.0", "isort>=5.13.2", "pygments>=2.20.0", + "virtualenv>=21.7.13", "types-pyyaml>=6.0.12.20260408", ] diff --git a/requirements.txt b/requirements.txt index 2373885..1337dc2 100644 --- a/requirements.txt +++ b/requirements.txt @@ -30,4 +30,4 @@ pytokens==0.4.1 pyyaml==6.0.3 rich==15.0.0 typing-extensions==4.15.0 -virtualenv==21.2.4 +virtualenv==21.7.16 diff --git a/src/fastapi_fastkit/__init__.py b/src/fastapi_fastkit/__init__.py index bf25615..5b60188 100644 --- a/src/fastapi_fastkit/__init__.py +++ b/src/fastapi_fastkit/__init__.py @@ -1 +1 @@ -__version__ = "1.4.1" +__version__ = "1.5.0" diff --git a/uv.lock b/uv.lock index d68dfa5..85d461d 100644 --- a/uv.lock +++ b/uv.lock @@ -323,6 +323,7 @@ dev = [ { name = "pytest" }, { name = "pytest-cov" }, { name = "types-pyyaml" }, + { name = "virtualenv" }, ] [package.dev-dependencies] @@ -358,6 +359,7 @@ requires-dist = [ { name = "pytest-cov", marker = "extra == 'dev'", specifier = ">=6.2.1" }, { name = "rich", specifier = ">=13.9.2" }, { name = "types-pyyaml", marker = "extra == 'dev'", specifier = ">=6.0.12.20260408" }, + { name = "virtualenv", marker = "extra == 'dev'", specifier = ">=21.7.13" }, ] provides-extras = ["dev"] @@ -1239,7 +1241,7 @@ wheels = [ [[package]] name = "virtualenv" -version = "21.7.9" +version = "21.7.16" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "distlib" }, @@ -1247,9 +1249,9 @@ dependencies = [ { name = "platformdirs" }, { name = "python-discovery" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/52/5c/ba82fdd0da13ade01453fc08277a70abc79128101319549f55f2e13f8f83/virtualenv-21.7.9.tar.gz", hash = "sha256:a7e42d81d779dec8afd7dc4be71640fb959ea861bccfa5980cb4ad9f92e30675", size = 5348882, upload-time = "2026-09-09T01:03:27.752Z" } +sdist = { url = "https://files.pythonhosted.org/packages/45/2b/f7ec483cb56fc84cab8676971b171156111cd125d069ca02fdaf0239f3e5/virtualenv-21.7.16.tar.gz", hash = "sha256:d3fcf6954a07121c2b61efcb96419e2f40b3718d558998f13a7314e32cc42723", size = 5363081, upload-time = "2026-09-19T06:59:08.339Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/85/fc/888c80b8da917bfc48a41887eca244f995d190bd3c01175727089fcaf27e/virtualenv-21.7.9-py3-none-any.whl", hash = "sha256:ba3b0bb41063c848d84d76a9fe3fb7711aaa0f2fe78708e8f3cd9714770e4eac", size = 5324667, upload-time = "2026-09-09T01:03:26.023Z" }, + { url = "https://files.pythonhosted.org/packages/c7/46/a02c0bb29e6102616aa91d68a5e3f055393d92353d94b52ad6c51d540c20/virtualenv-21.7.16-py3-none-any.whl", hash = "sha256:752fd5d5352502fbd794422983eed65a249fc6f4685c5e2122fd6052e6477a11", size = 5330278, upload-time = "2026-09-19T06:59:06.096Z" }, ] [[package]]