From dbaf2420434bd5861d0715db33223fbe61534a90 Mon Sep 17 00:00:00 2001 From: hideyukiMORI Date: Tue, 19 May 2026 21:56:09 +0900 Subject: [PATCH] =?UTF-8?q?docs:=20i18n=20=E5=AF=BE=E5=BF=9C=20=E2=80=94?= =?UTF-8?q?=20EN/JA=20=E5=AE=8C=E5=85=A8=E7=BF=BB=E8=A8=B3=20+=20FR/ZH/PT-?= =?UTF-8?q?BR/DE=20=E6=9C=80=E5=B0=8F=E3=83=9A=E3=83=BC=E3=82=B8=E3=82=92?= =?UTF-8?q?=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - docs/ ルートを英語(デフォルトロケール)に変換 - docs/ja/ に全ページの日本語版を追加(チュートリアル/ハウツー/解説/リファレンス) - docs/fr/ docs/zh/ docs/pt-br/ docs/de/ に最小ページ(index + getting-started)を追加 - .vitepress/config.mts を VitePress locales 設定に更新(6言語対応) Co-Authored-By: Claude Sonnet 4.6 --- .vitepress/config.mts | 218 +++++++++++++++++++--- docs/de/index.md | 43 +++++ docs/de/tutorials/getting-started.md | 54 ++++++ docs/explanation/architecture.md | 81 ++++---- docs/explanation/design-philosophy.md | 98 +++++----- docs/fr/index.md | 43 +++++ docs/fr/tutorials/getting-started.md | 54 ++++++ docs/how-to/add-new-domain.md | 62 ++++--- docs/how-to/configure-auth.md | 51 +++--- docs/how-to/run-tests.md | 75 ++++---- docs/howto/mcp-setup.md | 77 ++++---- docs/ja/explanation/architecture.md | 100 ++++++++++ docs/ja/explanation/design-philosophy.md | 68 +++++++ docs/ja/how-to/add-new-domain.md | 87 +++++++++ docs/ja/how-to/configure-auth.md | 101 ++++++++++ docs/ja/how-to/run-tests.md | 98 ++++++++++ docs/ja/howto/mcp-setup.md | 77 ++++++++ docs/ja/index.md | 43 +++++ docs/ja/reference/api.md | 137 ++++++++++++++ docs/ja/reference/configuration.md | 105 +++++++++++ docs/ja/reference/framework-modules.md | 224 +++++++++++++++++++++++ docs/ja/tutorials/first-domain.md | 154 ++++++++++++++++ docs/ja/tutorials/getting-started.md | 55 ++++++ docs/pt-br/index.md | 43 +++++ docs/pt-br/tutorials/getting-started.md | 54 ++++++ docs/reference/api.md | 114 +++++------- docs/reference/configuration.md | 89 +++++---- docs/reference/framework-modules.md | 136 +++++++------- docs/tutorials/first-domain.md | 62 +++---- docs/tutorials/getting-started.md | 36 ++-- docs/zh/index.md | 43 +++++ docs/zh/tutorials/getting-started.md | 54 ++++++ 32 files changed, 2270 insertions(+), 466 deletions(-) create mode 100644 docs/de/index.md create mode 100644 docs/de/tutorials/getting-started.md create mode 100644 docs/fr/index.md create mode 100644 docs/fr/tutorials/getting-started.md create mode 100644 docs/ja/explanation/architecture.md create mode 100644 docs/ja/explanation/design-philosophy.md create mode 100644 docs/ja/how-to/add-new-domain.md create mode 100644 docs/ja/how-to/configure-auth.md create mode 100644 docs/ja/how-to/run-tests.md create mode 100644 docs/ja/howto/mcp-setup.md create mode 100644 docs/ja/index.md create mode 100644 docs/ja/reference/api.md create mode 100644 docs/ja/reference/configuration.md create mode 100644 docs/ja/reference/framework-modules.md create mode 100644 docs/ja/tutorials/first-domain.md create mode 100644 docs/ja/tutorials/getting-started.md create mode 100644 docs/pt-br/index.md create mode 100644 docs/pt-br/tutorials/getting-started.md create mode 100644 docs/zh/index.md create mode 100644 docs/zh/tutorials/getting-started.md diff --git a/.vitepress/config.mts b/.vitepress/config.mts index c0a8203..0728899 100644 --- a/.vitepress/config.mts +++ b/.vitepress/config.mts @@ -1,11 +1,11 @@ import { defineConfig } from 'vitepress' -function nav() { +function navEn() { return [ - { text: 'Tutorial', link: '/tutorials/getting-started', activeMatch: 'tutorials/' }, - { text: 'How-to', link: '/how-to/add-new-domain', activeMatch: 'how-to/' }, - { text: 'Explanation', link: '/explanation/architecture', activeMatch: 'explanation/' }, - { text: 'Reference', link: '/reference/configuration', activeMatch: 'reference/' }, + { text: 'Tutorial', link: '/tutorials/getting-started', activeMatch: '/tutorials/' }, + { text: 'How-to', link: '/how-to/add-new-domain', activeMatch: '/how-to/' }, + { text: 'Explanation', link: '/explanation/architecture', activeMatch: '/explanation/' }, + { text: 'Reference', link: '/reference/configuration', activeMatch: '/reference/' }, { text: 'v1.0.0', items: [ @@ -17,29 +17,46 @@ function nav() { ] } -function sidebar() { +function navJa() { + return [ + { text: 'チュートリアル', link: '/ja/tutorials/getting-started', activeMatch: '/ja/tutorials/' }, + { text: 'ハウツー', link: '/ja/how-to/add-new-domain', activeMatch: '/ja/how-to/' }, + { text: '解説', link: '/ja/explanation/architecture', activeMatch: '/ja/explanation/' }, + { text: 'リファレンス', link: '/ja/reference/configuration', activeMatch: '/ja/reference/' }, + { + text: 'v1.0.0', + items: [ + { text: '変更履歴', link: 'https://github.com/hideyukiMORI/nene2-python/blob/main/CHANGELOG.md' }, + { text: 'リリース', link: 'https://github.com/hideyukiMORI/nene2-python/releases' }, + { text: 'PHP NENE2', link: 'https://hideyukimori.github.io/NENE2/' }, + ], + }, + ] +} + +function sidebarEn() { return { '/tutorials/': [{ text: 'Tutorials', items: [ - { text: 'Getting started', link: '/tutorials/getting-started' }, + { text: 'Getting started', link: '/tutorials/getting-started' }, { text: 'Implement a new domain', link: '/tutorials/first-domain' }, ], }], '/how-to/': [{ text: 'How-to guides', items: [ - { text: 'Add a new domain', link: '/how-to/add-new-domain' }, - { text: 'Configure auth', link: '/how-to/configure-auth' }, - { text: 'Set up MCP', link: '/howto/mcp-setup' }, - { text: 'Run tests', link: '/how-to/run-tests' }, + { text: 'Add a new domain', link: '/how-to/add-new-domain' }, + { text: 'Configure auth', link: '/how-to/configure-auth' }, + { text: 'Set up MCP', link: '/howto/mcp-setup' }, + { text: 'Run tests', link: '/how-to/run-tests' }, ], }], '/explanation/': [{ text: 'Explanation', items: [ - { text: 'Architecture', link: '/explanation/architecture' }, - { text: 'Design philosophy', link: '/explanation/design-philosophy' }, + { text: 'Architecture', link: '/explanation/architecture' }, + { text: 'Design philosophy', link: '/explanation/design-philosophy' }, ], }, { text: 'ADR', @@ -58,9 +75,9 @@ function sidebar() { '/reference/': [{ text: 'Reference', items: [ - { text: 'Configuration', link: '/reference/configuration' }, - { text: 'Framework modules', link: '/reference/framework-modules' }, - { text: 'REST API', link: '/reference/api' }, + { text: 'Configuration', link: '/reference/configuration' }, + { text: 'Framework modules', link: '/reference/framework-modules' }, + { text: 'REST API', link: '/reference/api' }, ], }], '/adr/': [{ @@ -79,6 +96,42 @@ function sidebar() { } } +function sidebarJa() { + return { + '/ja/tutorials/': [{ + text: 'チュートリアル', + items: [ + { text: 'はじめての nene2-python', link: '/ja/tutorials/getting-started' }, + { text: '新しいドメインを実装する', link: '/ja/tutorials/first-domain' }, + ], + }], + '/ja/how-to/': [{ + text: 'ハウツーガイド', + items: [ + { text: '新しいドメインを追加する', link: '/ja/how-to/add-new-domain' }, + { text: '認証を設定する', link: '/ja/how-to/configure-auth' }, + { text: 'MCP セットアップ', link: '/ja/howto/mcp-setup' }, + { text: 'テストを実行する', link: '/ja/how-to/run-tests' }, + ], + }], + '/ja/explanation/': [{ + text: '解説', + items: [ + { text: 'アーキテクチャ概要', link: '/ja/explanation/architecture' }, + { text: '設計思想と PHP との対応', link: '/ja/explanation/design-philosophy' }, + ], + }], + '/ja/reference/': [{ + text: 'リファレンス', + items: [ + { text: '設定リファレンス', link: '/ja/reference/configuration' }, + { text: 'フレームワークモジュール', link: '/ja/reference/framework-modules' }, + { text: 'REST API', link: '/ja/reference/api' }, + ], + }], + } +} + export default defineConfig({ title: 'NENE2 Python', description: 'FastAPI + Clean Architecture + MCP. Python 3.12+. AI-ready from day one.', @@ -93,27 +146,132 @@ export default defineConfig({ ['link', { rel: 'icon', href: 'data:image/svg+xml,🐍' }], ], + locales: { + root: { + label: 'English', + lang: 'en', + themeConfig: { + nav: navEn(), + sidebar: sidebarEn(), + editLink: { + pattern: 'https://github.com/hideyukiMORI/nene2-python/edit/main/docs/:path', + text: 'Edit this page on GitHub', + }, + footer: { + message: 'Released under the MIT License.', + copyright: 'Copyright © 2026 hideyukiMORI', + }, + }, + }, + ja: { + label: '日本語', + lang: 'ja', + themeConfig: { + nav: navJa(), + sidebar: sidebarJa(), + editLink: { + pattern: 'https://github.com/hideyukiMORI/nene2-python/edit/main/docs/:path', + text: 'GitHub でこのページを編集', + }, + footer: { + message: 'MIT ライセンスの下でリリースされています。', + copyright: 'Copyright © 2026 hideyukiMORI', + }, + }, + }, + fr: { + label: 'Français', + lang: 'fr', + themeConfig: { + nav: [ + { text: 'Tutoriel', link: '/fr/tutorials/getting-started' }, + { text: 'v1.0.0', items: [{ text: 'Releases', link: 'https://github.com/hideyukiMORI/nene2-python/releases' }] }, + ], + sidebar: { + '/fr/': [{ text: 'Tutoriels', items: [{ text: 'Premiers pas', link: '/fr/tutorials/getting-started' }] }], + }, + editLink: { + pattern: 'https://github.com/hideyukiMORI/nene2-python/edit/main/docs/:path', + text: 'Éditer cette page sur GitHub', + }, + footer: { + message: 'Publié sous la licence MIT.', + copyright: 'Copyright © 2026 hideyukiMORI', + }, + }, + }, + zh: { + label: '简体中文', + lang: 'zh-CN', + themeConfig: { + nav: [ + { text: '教程', link: '/zh/tutorials/getting-started' }, + { text: 'v1.0.0', items: [{ text: 'Releases', link: 'https://github.com/hideyukiMORI/nene2-python/releases' }] }, + ], + sidebar: { + '/zh/': [{ text: '教程', items: [{ text: '快速开始', link: '/zh/tutorials/getting-started' }] }], + }, + editLink: { + pattern: 'https://github.com/hideyukiMORI/nene2-python/edit/main/docs/:path', + text: '在 GitHub 上编辑此页面', + }, + footer: { + message: '根据 MIT 许可证发布。', + copyright: 'Copyright © 2026 hideyukiMORI', + }, + }, + }, + 'pt-br': { + label: 'Português (Brasil)', + lang: 'pt-BR', + themeConfig: { + nav: [ + { text: 'Tutorial', link: '/pt-br/tutorials/getting-started' }, + { text: 'v1.0.0', items: [{ text: 'Releases', link: 'https://github.com/hideyukiMORI/nene2-python/releases' }] }, + ], + sidebar: { + '/pt-br/': [{ text: 'Tutoriais', items: [{ text: 'Primeiros passos', link: '/pt-br/tutorials/getting-started' }] }], + }, + editLink: { + pattern: 'https://github.com/hideyukiMORI/nene2-python/edit/main/docs/:path', + text: 'Editar esta página no GitHub', + }, + footer: { + message: 'Lançado sob a Licença MIT.', + copyright: 'Copyright © 2026 hideyukiMORI', + }, + }, + }, + de: { + label: 'Deutsch', + lang: 'de', + themeConfig: { + nav: [ + { text: 'Tutorial', link: '/de/tutorials/getting-started' }, + { text: 'v1.0.0', items: [{ text: 'Releases', link: 'https://github.com/hideyukiMORI/nene2-python/releases' }] }, + ], + sidebar: { + '/de/': [{ text: 'Tutorials', items: [{ text: 'Erste Schritte', link: '/de/tutorials/getting-started' }] }], + }, + editLink: { + pattern: 'https://github.com/hideyukiMORI/nene2-python/edit/main/docs/:path', + text: 'Diese Seite auf GitHub bearbeiten', + }, + footer: { + message: 'Veröffentlicht unter der MIT-Lizenz.', + copyright: 'Copyright © 2026 hideyukiMORI', + }, + }, + }, + }, + themeConfig: { siteTitle: '🐍 NENE2', - nav: nav(), - sidebar: sidebar(), - socialLinks: [ { icon: 'github', link: 'https://github.com/hideyukiMORI/nene2-python' }, ], - search: { provider: 'local' }, outline: { level: [2, 3] }, - - editLink: { - pattern: 'https://github.com/hideyukiMORI/nene2-python/edit/main/docs/:path', - text: 'Edit this page on GitHub', - }, - - footer: { - message: 'Released under the MIT License.', - copyright: 'Copyright © 2026 hideyukiMORI', - }, }, markdown: { diff --git a/docs/de/index.md b/docs/de/index.md new file mode 100644 index 0000000..a0c41a9 --- /dev/null +++ b/docs/de/index.md @@ -0,0 +1,43 @@ +--- +layout: home + +hero: + name: "NENE2" + text: "Python API Framework" + tagline: FastAPI · Clean Architecture · MCP · mypy --strict · Von Anfang an KI-bereit. + actions: + - theme: brand + text: Loslegen → + link: /de/tutorials/getting-started + - theme: alt + text: Auf GitHub ansehen + link: https://github.com/hideyukiMORI/nene2-python + - theme: alt + text: PHP-Version + link: https://hideyukimori.github.io/NENE2/ + +features: + - icon: 🐍 + title: Python 3.12+ nativ + details: Python 3.12 generische Syntax, eingefrorene Dataclasses und Pydantic v2. mypy --strict bei jedem Commit erzwungen. + + - icon: ⚡ + title: FastAPI + async + details: ASGI-nativ mit AsyncUseCaseProtocol für nicht blockierendes I/O. asyncio.gather für parallele Repository-Aufrufe. + + - icon: 🤖 + title: MCP integriert + details: UseCases werden über LocalMcpServer als MCP-Werkzeuge bereitgestellt — ohne zusätzliche Konfiguration. + + - icon: 🏛️ + title: Clean Architecture + details: HTTP Handler → UseCase → RepositoryInterface → SQLAlchemy. Jede Schicht mit InMemory-Repositories testbar. + + - icon: 🛡️ + title: Sicherheit zuerst + details: RFC 9457 Problem Details, Bearer + API Key Authentifizierung, Rate Limiting, Sicherheits-Header — sofort einsatzbereit. + + - icon: 📄 + title: OpenAPI automatisch generiert + details: Swagger UI und ReDoc unter /docs — keine Konfiguration. Statisches openapi.yaml mit einem Befehl exportieren. +--- diff --git a/docs/de/tutorials/getting-started.md b/docs/de/tutorials/getting-started.md new file mode 100644 index 0000000..1e12dbd --- /dev/null +++ b/docs/de/tutorials/getting-started.md @@ -0,0 +1,54 @@ +# Erste Schritte mit nene2-python + +Dieses Tutorial ermöglicht es Ihnen, in 5 Minuten eine Notes-CRUD-API mit nene2-python zu starten. + +## Voraussetzungen + +- Python 3.12 oder höher +- [uv](https://docs.astral.sh/uv/) installiert +- Git + +## 1. Repository klonen + +```bash +git clone https://github.com/hideyukiMORI/nene2-python.git +cd nene2-python +``` + +## 2. Abhängigkeiten installieren + +```bash +uv sync +``` + +## 3. Entwicklungsserver starten + +```bash +uv run uvicorn src.example.app:app --reload --port 8080 +``` + +Öffnen Sie `http://localhost:8080/docs` im Browser für die Swagger UI. + +## 4. API testen + +```bash +# Notiz erstellen +curl -X POST http://localhost:8080/notes \ + -H "Content-Type: application/json" \ + -d '{"title": "Meine erste Notiz", "body": "Erstellt mit nene2-python"}' + +# Notizen auflisten +curl http://localhost:8080/notes +``` + +## 5. Tests ausführen + +```bash +uv run pytest +``` + +Mehr als 135 Tests sollten erfolgreich sein. + +## Nächste Schritte + +- [Konfigurationsreferenz](../reference/configuration.md) — Datenbank und Authentifizierung über Umgebungsvariablen konfigurieren diff --git a/docs/explanation/architecture.md b/docs/explanation/architecture.md index b75e099..ab79868 100644 --- a/docs/explanation/architecture.md +++ b/docs/explanation/architecture.md @@ -1,8 +1,8 @@ -# アーキテクチャ概要 +# Architecture overview -## レイヤー構造 +## Layer structure -nene2-python はクリーンアーキテクチャに基づいており、依存関係は外から内へ向かいます。 +nene2-python follows Clean Architecture. Dependencies flow from the outside in. ``` ┌─────────────────────────────────────────────┐ @@ -10,24 +10,25 @@ nene2-python はクリーンアーキテクチャに基づいており、依存 │ parse request → call use-case → response │ ├─────────────────────────────────────────────┤ │ UseCase │ -│ ビジネスロジック。HTTP・DB を知らない │ +│ Business logic — no HTTP or DB knowledge │ ├─────────────────────────────────────────────┤ │ RepositoryInterface (ABC) │ -│ ドメインが必要とする操作の契約 │ +│ Contract for the operations the domain │ +│ needs │ ├─────────────────────────────────────────────┤ │ ConcreteRepository │ -│ SQLAlchemy / InMemory 実装 │ +│ SQLAlchemy / InMemory implementations │ └─────────────────────────────────────────────┘ ``` -## 各レイヤーの責務 +## Layer responsibilities ### HTTP Handler -- **唯一の責務**: リクエストを解析し UseCase を呼び、レスポンスを返す -- Pydantic `BaseModel` でリクエストボディを検証(HTTP 境界のみ) -- ドメインロジックを持たない -- `make_xxx_router()` ファクトリ関数がルーターを返す +- **Single responsibility**: parse the request, call a UseCase, return a response +- Uses Pydantic `BaseModel` for request body validation (HTTP boundary only) +- Contains zero domain logic +- Exposed via a `make_xxx_router()` factory function ```python @router.post("", status_code=201) @@ -38,46 +39,46 @@ async def create_note(body: CreateNoteBody) -> JSONResponse: ### UseCase -- **唯一の責務**: ビジネスルールを実装する -- `execute(input_: XxxInput) -> XxxOutput` の単一メソッド -- `import fastapi`, `import sqlalchemy` を持たない -- 他の UseCase を呼ばない(オーケストレーションは上位層) -- `InMemoryRepository` でテスト可能 +- **Single responsibility**: implement one business rule +- One method: `execute(input_: XxxInput) -> XxxOutput` +- No `import fastapi`, no `import sqlalchemy` +- Does not call other UseCases +- Testable with `InMemoryRepository` alone ### RepositoryInterface -- ABC で契約を定義 -- UseCase は Interface のみに依存する(具象クラスを知らない) -- InMemory 実装と SQLAlchemy 実装が同じ Interface を実装する +- Defined as an ABC — the UseCase depends only on the interface +- Same interface is implemented by InMemory and SQLAlchemy versions +- `find_all`, `find_by_id`, `save`, `update`, `delete`, `count` ### ConcreteRepository -- SQLAlchemy Core(ORM なし)でパラメータ化クエリを実行 -- `SqlAlchemyQueryExecutor` でクエリを抽象化 -- テーブルスキーマは `src/example/schema.py` で一元管理 +- SQLAlchemy Core (no ORM) with parameterised queries +- Queries are executed via `SqlAlchemyQueryExecutor` +- Table schema is managed centrally in `src/example/schema.py` -## ミドルウェアスタック +## Middleware stack -リクエストは外側から内側に向かって処理されます: +Requests pass through each middleware from outermost to innermost: ``` -BearerTokenMiddleware 認証 (Bearer Token) -ApiKeyAuthMiddleware 認証 (API Key) +BearerTokenMiddleware Authentication (Bearer Token) +ApiKeyAuthMiddleware Authentication (API Key) CORSMiddleware CORS -ThrottleMiddleware レートリミット -RequestSizeLimitMiddleware ペイロードサイズ制限 -RequestLoggingMiddleware リクエストロギング -RequestIdMiddleware リクエスト ID 付与 -SecurityHeadersMiddleware セキュリティヘッダー付与 -ErrorHandlerMiddleware 例外 → RFC 9457 Problem Details 変換 +ThrottleMiddleware Rate limiting (fixed window) +RequestSizeLimitMiddleware Payload size enforcement +RequestLoggingMiddleware Structured request logging (structlog) +RequestIdMiddleware X-Request-ID generation / propagation +SecurityHeadersMiddleware Security response headers +ErrorHandlerMiddleware Exceptions → RFC 9457 Problem Details ``` -## DI パターン +## Dependency injection -FastAPI の `Depends` は HTTP 境界のみで使用します。UseCase とリポジトリはコンストラクタインジェクションで接続します。 +FastAPI's `Depends` is used at the HTTP boundary only. UseCases and repositories are wired via constructor injection in `app.py`. ```python -# app.py — ワイヤリング +# app.py — wiring note_repo = SqlAlchemyNoteRepository(executor) app.include_router(make_note_router( list_use_case=ListNotesUseCase(note_repo), @@ -86,15 +87,15 @@ app.include_router(make_note_router( )) ``` -## ドメインパッケージ構造 +## Domain package layout ``` src/example// __init__.py entity.py — @dataclass(frozen=True, slots=True) - repository.py — ABC + InMemory 実装 + repository.py — ABC + InMemory implementation exceptions.py — XxxNotFoundException + ExceptionHandler - use_case.py — 5 UseCase + Input/Output DTO - handler.py — FastAPI router - sqlalchemy_repository.py — SQL バックエンド + use_case.py — 5 UseCases + Input/Output DTOs + handler.py — FastAPI router factory + sqlalchemy_repository.py — SQL backend ``` diff --git a/docs/explanation/design-philosophy.md b/docs/explanation/design-philosophy.md index 4bcb0f1..38780c7 100644 --- a/docs/explanation/design-philosophy.md +++ b/docs/explanation/design-philosophy.md @@ -1,68 +1,68 @@ -# 設計思想と PHP NENE2 との対応 +# Design philosophy -## NENE2 の設計原則 +## NENE2 core principles -nene2-python は PHP 版 NENE2 と同一の設計思想を持ちます。 +nene2-python shares the same design philosophy as PHP NENE2. ### API First -JSON API と OpenAPI 契約を中心に据えます。DB 設計より先に API の形を定義し、スキーマを `uv run export-openapi` で生成します。 +The JSON API contract and OpenAPI schema are defined before the database schema. Use `uv run export-openapi` to export a static `openapi.yaml` at any time. -### 薄い HTTP 層 +### Thin HTTP layer -HTTP Handler はビジネスロジックを持ちません。**parse → use-case → response** の 3 ステップのみ。ドメインルールは UseCase に集約されます。 +HTTP Handlers own no business logic. The rule is: **parse → use-case → response** — three steps, nothing more. Domain rules live in UseCases. ### AI-readable -明示的なディレクトリ構造、小さなクラス(150 行以下)、型付き境界により、LLM がコードベースを正確に理解・操作できます。 +Explicit directory structure, small classes (≤ 150 lines), typed boundaries — these let an LLM navigate and modify the codebase with confidence. -### Security First +### Security first -セキュリティは後付けではなく設計の出発点です。 -- Pydantic による HTTP 境界の全入力検証 -- パラメータ化クエリのみ(SQLインジェクション防止) -- `secrets.compare_digest` によるタイミング安全な比較 -- セキュリティヘッダーをミドルウェアで付与 +Security is a design constraint, not an afterthought: +- All HTTP inputs validated by Pydantic at the boundary +- Parameterised queries only (SQL injection prevention) +- `secrets.compare_digest` for timing-safe token comparison +- Security headers applied by middleware on every response ### LLM Delivery Ready -UseCase は HTTP・DB から独立しているため、MCP ツールとして直接再利用できます。`src/example/mcp.py` はその実証です。 +Because UseCases are independent of HTTP and database, they can be registered directly as MCP tools. `src/example/mcp.py` proves this — 15 tools, zero extra plumbing. -## PHP NENE2 との対応表 +--- -| PHP 版 | Python 版 | 備考 | -|---|---|---| -| `readonly class` | `@dataclass(frozen=True, slots=True)` | 不変 Value Object | -| `ValidationException` + `ValidationError` | 同名クラス (`nene2.validation`) | 422 + Problem Details | -| `PaginationQueryParser` | `nene2.http.PaginationQueryParser` | クエリパラメータ解析 | -| `PaginationResponse` | `nene2.http.PaginationResponse` | ページネーションレスポンス | -| `ProblemDetailsResponseFactory` | `nene2.http.problem_details_response()` | RFC 9457 | -| `ErrorHandlerMiddleware` | `nene2.middleware.ErrorHandlerMiddleware` | 全例外をキャッチ | -| `PHPStan level 8` | `mypy --strict` | 最高レベルの型チェック | -| `PHP-CS-Fixer` | `ruff format` | コードフォーマット | -| `UseCaseInterface` | `nene2.use_case.UseCaseProtocol[I, O]` | 構造的サブタイピング | +## Python vs PHP NENE2 -## Python 3.12+ 固有の選択 - -| 用途 | 選択 | 理由 | +| PHP | Python | Notes | |---|---|---| -| 型エイリアス | `type X = list[str]` | PEP 695 — 新構文 | -| ジェネリクス | `class Foo[T]` | PEP 695 — TypeVar 不要 | -| 不変 VO | `dataclass(frozen=True, slots=True)` | メモリ効率 + 不変性 | -| HTTP 検証 | Pydantic v2 BaseModel | 高速 + 型安全 | -| SQL | SQLAlchemy Core | ORM なしで SQL を直接制御 | -| ロギング | structlog | JSON / Console の両対応 | -| MCP | mcp (Anthropic SDK) | FastMCP ラッパー | - -## ADR 一覧 - -設計の個別決定は ADR に記録されています: - -- [ADR-0001: ツールチェーン](../adr/0001-toolchain.md) -- [ADR-0002: クリーンアーキテクチャ](../adr/0002-clean-architecture.md) -- [ADR-0003: セキュリティファースト](../adr/0003-security-first.md) -- [ADR-0004: AI ファースト設計](../adr/0004-ai-first-design.md) -- [ADR-0005: ロギング](../adr/0005-logging.md) -- [ADR-0006: レートリミット](../adr/0006-rate-limiting.md) -- [ADR-0009: MCP 設計](../adr/0009-mcp-design.md) -- [ADR-0010: AsyncUseCase パターン](../adr/0010-async-use-case.md) +| `readonly class` | `@dataclass(frozen=True, slots=True)` | Immutable value object | +| `ValidationException` + `ValidationError` | Same names (`nene2.validation`) | 422 + Problem Details | +| `PaginationQueryParser` | `nene2.http.PaginationQueryParser` | Query param parsing | +| `PaginationResponse` | `nene2.http.PaginationResponse` | Paginated response | +| `ProblemDetailsResponseFactory` | `nene2.http.problem_details_response()` | RFC 9457 | +| `ErrorHandlerMiddleware` | `nene2.middleware.ErrorHandlerMiddleware` | Catches all exceptions | +| `PHPStan level 8` | `mypy --strict` | Maximum type safety | +| `PHP-CS-Fixer` | `ruff format` | Code formatting | +| `UseCaseInterface` | `nene2.use_case.UseCaseProtocol[I, O]` | Structural typing | + +## Python-only features + +| Feature | Why Python wins | +|---|---| +| `AsyncUseCaseProtocol[I, O]` | No PHP equivalent — native coroutine protocol | +| OpenAPI auto-generation | FastAPI generates Swagger UI / ReDoc with zero config | +| Native async/await | FastAPI + uvicorn — non-blocking I/O throughout | +| MCP SDK | Anthropic's Python SDK is the reference implementation | +| `mypy --strict` | Tighter than PHPStan level 8 in practice | + +## ADR index + +Individual design decisions are recorded in Architecture Decision Records: + +- [ADR-0001: Toolchain](../adr/0001-toolchain) +- [ADR-0002: Clean Architecture](../adr/0002-clean-architecture) +- [ADR-0003: Security First](../adr/0003-security-first) +- [ADR-0004: AI-First Design](../adr/0004-ai-first-design) +- [ADR-0005: Logging](../adr/0005-logging) +- [ADR-0006: Rate Limiting](../adr/0006-rate-limiting) +- [ADR-0009: MCP Design](../adr/0009-mcp-design) +- [ADR-0010: AsyncUseCase Pattern](../adr/0010-async-use-case) diff --git a/docs/fr/index.md b/docs/fr/index.md new file mode 100644 index 0000000..ac2f44c --- /dev/null +++ b/docs/fr/index.md @@ -0,0 +1,43 @@ +--- +layout: home + +hero: + name: "NENE2" + text: "Framework API Python" + tagline: FastAPI · Architecture propre · MCP · mypy --strict · Conçu pour l'IA dès le premier jour. + actions: + - theme: brand + text: Commencer → + link: /fr/tutorials/getting-started + - theme: alt + text: Voir sur GitHub + link: https://github.com/hideyukiMORI/nene2-python + - theme: alt + text: Version PHP + link: https://hideyukimori.github.io/NENE2/ + +features: + - icon: 🐍 + title: Python 3.12+ natif + details: Syntaxe générique Python 3.12, dataclasses gelées et Pydantic v2. mypy --strict appliqué à chaque commit. + + - icon: ⚡ + title: FastAPI + async + details: ASGI natif avec AsyncUseCaseProtocol pour les I/O non bloquants. asyncio.gather pour les appels parallèles. + + - icon: 🤖 + title: MCP intégré + details: Les UseCases sont exposés comme outils MCP via LocalMcpServer — sans configuration supplémentaire. + + - icon: 🏛️ + title: Architecture propre + details: HTTP Handler → UseCase → RepositoryInterface → SQLAlchemy. Chaque couche est testable en isolation. + + - icon: 🛡️ + title: Sécurité d'abord + details: RFC 9457 Problem Details, authentification Bearer + API Key, limitation de débit, en-têtes de sécurité. + + - icon: 📄 + title: OpenAPI auto-généré + details: Swagger UI et ReDoc à /docs — sans configuration. Export d'un openapi.yaml statique en une commande. +--- diff --git a/docs/fr/tutorials/getting-started.md b/docs/fr/tutorials/getting-started.md new file mode 100644 index 0000000..e44010c --- /dev/null +++ b/docs/fr/tutorials/getting-started.md @@ -0,0 +1,54 @@ +# Premiers pas avec nene2-python + +Ce tutoriel vous permet de démarrer une API CRUD Notes avec nene2-python en 5 minutes. + +## Prérequis + +- Python 3.12 ou supérieur +- [uv](https://docs.astral.sh/uv/) installé +- Git + +## 1. Cloner le dépôt + +```bash +git clone https://github.com/hideyukiMORI/nene2-python.git +cd nene2-python +``` + +## 2. Installer les dépendances + +```bash +uv sync +``` + +## 3. Démarrer le serveur de développement + +```bash +uv run uvicorn src.example.app:app --reload --port 8080 +``` + +Ouvrez `http://localhost:8080/docs` dans votre navigateur pour accéder à Swagger UI. + +## 4. Tester l'API + +```bash +# Créer une note +curl -X POST http://localhost:8080/notes \ + -H "Content-Type: application/json" \ + -d '{"title": "Ma première note", "body": "Créée avec nene2-python"}' + +# Lister les notes +curl http://localhost:8080/notes +``` + +## 5. Exécuter les tests + +```bash +uv run pytest +``` + +Plus de 135 tests doivent tous réussir. + +## Étapes suivantes + +- [Référence de configuration](../reference/configuration.md) — Configurer la base de données et l'authentification via les variables d'environnement diff --git a/docs/how-to/add-new-domain.md b/docs/how-to/add-new-domain.md index d866cd5..e297bab 100644 --- a/docs/how-to/add-new-domain.md +++ b/docs/how-to/add-new-domain.md @@ -1,30 +1,30 @@ -# 新しいドメインを追加する +# Add a new domain -既存の Note・Tag・Comment ドメインと同じパターンで新しいドメインを追加するチェックリストです。 +A checklist for adding a new domain following the same pattern as Note, Tag, and Comment. -## チェックリスト +## Checklist -### 1. ドメインパッケージを作成する +### 1. Create the domain package ```bash mkdir -p src/example/ touch src/example//__init__.py ``` -### 2. 各ファイルを作成する +### 2. Create each file -| ファイル | 内容 | +| File | Content | |---|---| -| `entity.py` | `@dataclass(frozen=True, slots=True)` でエンティティを定義 | +| `entity.py` | Entity as `@dataclass(frozen=True, slots=True)` | | `repository.py` | `XxxRepositoryInterface(ABC)` + `InMemoryXxxRepository` | | `exceptions.py` | `XxxNotFoundException` + `XxxNotFoundExceptionHandler` | -| `use_case.py` | 5 UseCase (List / Get / Create / Update / Delete) + Input/Output DTO | +| `use_case.py` | 5 UseCases (List / Get / Create / Update / Delete) + Input/Output DTOs | | `handler.py` | `make_xxx_router()` — parse → use-case → response | -| `sqlalchemy_repository.py` | SQL バックエンド実装 | +| `sqlalchemy_repository.py` | SQL backend implementation | -### 3. schema.py にテーブルを追加する +### 3. Add the table to schema.py -`src/example/schema.py` の `ensure_schema()` にテーブル定義を追加します。 +Add a `CREATE TABLE` call to `ensure_schema()` in `src/example/schema.py`. ```python executor.write( @@ -36,36 +36,36 @@ executor.write( ) ``` -### 4. app.py に組み込む +### 4. Wire into app.py -`src/example/app.py` の `_build_repositories()` と `create_app()` を更新します。 +Update `_build_repositories()` and `create_app()` in `src/example/app.py`. ```python -# _build_repositories() の戻り値に追加 +# Add to _build_repositories() return tuple your_repo = SqlAlchemyYourRepository(executor) -# create_app() でルーターを登録 +# Register the router in create_app() app.include_router(make_your_router( list_use_case=ListYourUseCase(your_repo), ... )) ``` -### 5. テストを書く +### 5. Write tests ``` tests/example// __init__.py - test__use_case.py # UseCase 単体テスト(DB なし) - test__repository.py # Repository 契約テスト(InMemory + SQLAlchemy) - test__http.py # HTTP 統合テスト(TestClient) + test__use_case.py # UseCase unit tests (no DB) + test__repository.py # Repository contract tests (InMemory + SQLAlchemy) + test__http.py # HTTP integration tests (TestClient) ``` -### 6. MCP ツールに追加する(任意) +### 6. Register MCP tools (optional) -`src/example/mcp.py` の `create_mcp_server()` に UseCase を登録します。 +Add UseCase registrations to `create_mcp_server()` in `src/example/mcp.py`. -### 7. 全チェックを通過させる +### 7. Pass all checks ```bash uv run pytest && \ @@ -74,14 +74,16 @@ uv run ruff check src/ tests/ && \ uv run ruff format --check src/ tests/ ``` -## 命名規則 +## Naming conventions -- エンティティクラス: `PascalCase` (`Note`, `Tag`, `Comment`) -- UseCase 入力 DTO: `XxxInput` (`CreateNoteInput`) -- 例外: `XxxNotFoundException` -- ハンドラーファクトリ: `make_xxx_router()` +| Target | Convention | Example | +|---|---|---| +| Entity class | PascalCase | `Note`, `Tag`, `Comment` | +| UseCase input DTO | `XxxInput` | `CreateNoteInput` | +| Exception | `XxxNotFoundException` | `NoteNotFoundException` | +| Handler factory | `make_xxx_router()` | `make_note_router()` | -## 参考実装 +## Reference implementations -- `src/example/note/` — 基本的な CRUD ドメイン -- `src/example/comment/` — 外部キー (note_id) を持つネストドメイン +- `src/example/note/` — basic CRUD domain +- `src/example/comment/` — nested domain with foreign key (`note_id`) diff --git a/docs/how-to/configure-auth.md b/docs/how-to/configure-auth.md index 3f4ec6b..a97c1c9 100644 --- a/docs/how-to/configure-auth.md +++ b/docs/how-to/configure-auth.md @@ -1,57 +1,55 @@ -# 認証を設定する +# Configure authentication -nene2-python は Bearer Token 認証と API Key 認証の 2 種類をサポートしています。 -どちらもミドルウェアとして実装されており、環境変数で有効化できます。 +nene2-python supports Bearer Token and API Key authentication, both implemented as middleware and enabled via environment variables. -## Bearer Token 認証 +## Bearer Token -### 有効化する +### Enable -`.env` ファイルに以下を追加します: +Add to your `.env` file: ```dotenv BEARER_TOKEN_ENABLED=true BEARER_TOKENS=token1,token2,token3 ``` -### 動作 +### Behaviour -- `Authorization: Bearer ` ヘッダーが必須になります -- トークンは `secrets.compare_digest` でタイミング安全な比較を行います -- 無効なトークンは `401 Unauthorized` を返します +- Every request must include `Authorization: Bearer ` +- Tokens are compared with `secrets.compare_digest` (timing-safe) +- Invalid tokens return `401 Unauthorized` (RFC 9457 Problem Details) -### curl での利用 +### Example ```bash curl -H "Authorization: Bearer token1" http://localhost:8080/notes ``` -## API Key 認証 +## API Key -### 有効化する +### Enable ```dotenv API_KEY_ENABLED=true API_KEYS=key1,key2 ``` -### 動作 +### Behaviour -- `X-Api-Key: ` ヘッダーが必須になります -- 無効なキーは `401 Unauthorized` を返します +- Every request must include `X-Api-Key: ` +- Invalid keys return `401 Unauthorized` -### curl での利用 +### Example ```bash curl -H "X-Api-Key: key1" http://localhost:8080/notes ``` -## 両方を有効化する場合 +## Using both at once -Bearer Token と API Key を同時に有効化すると、リクエストは両方の認証を通過する必要があります。 -通常は どちらか一方を使います。 +When both `BEARER_TOKEN_ENABLED` and `API_KEY_ENABLED` are set, requests must pass both checks. In practice you would choose one or the other. -## テスト時に認証を無効化する +## Disabling auth in tests ```python from nene2.config import AppSettings @@ -61,11 +59,12 @@ from example.app import create_app client = TestClient(create_app(AppSettings(bearer_token_enabled=False))) ``` -## カスタム TokenVerifier を実装する +## Custom TokenVerifier (e.g. JWT) -`TokenVerifierProtocol` を実装することで、JWT や外部サービスによる検証を追加できます。 +Implement `TokenVerifierProtocol` and raise `TokenVerificationException` on failure. ```python +from nene2.auth import TokenVerificationException from nene2.auth.interfaces import TokenVerifierProtocol import jwt @@ -77,8 +76,8 @@ class JwtTokenVerifier: try: jwt.decode(token, self._secret, algorithms=["HS256"]) return True - except jwt.InvalidTokenError: - return False + except jwt.InvalidTokenError as exc: + raise TokenVerificationException(str(exc)) from exc ``` -`create_app()` の `verifier` 引数に渡すか、`BearerTokenMiddleware` を直接使います。 +Pass your verifier directly to `BearerTokenMiddleware`. diff --git a/docs/how-to/run-tests.md b/docs/how-to/run-tests.md index a079759..25333e6 100644 --- a/docs/how-to/run-tests.md +++ b/docs/how-to/run-tests.md @@ -1,45 +1,48 @@ -# テストを実行する +# Run tests -## 基本コマンド +## Basic commands ```bash -# 全テストを実行(カバレッジ付き) +# Run all tests with coverage uv run pytest -# 失敗時の詳細表示 +# Verbose output on failure uv run pytest --tb=short -v -# 特定のファイルだけ実行 +# Run a specific directory uv run pytest tests/example/note/ -# カバレッジ HTML レポートを生成 +# Generate an HTML coverage report uv run pytest --cov=src --cov-report=html -# → htmlcov/index.html をブラウザで開く +# → open htmlcov/index.html in your browser ``` -## テスト構造 +## Test layout ``` tests/ - nene2/ フレームワークコアの単体テスト - use_case/ UseCaseProtocol 準拠テスト - ... + nene2/ Framework core unit tests + use_case/ UseCaseProtocol compliance + auth/ Auth middleware and verifiers + database/ TransactionManager tests + mcp/ McpHttpClient tests + middleware/ Each middleware in isolation example/ - note/ Note ドメインテスト - test_list_notes.py UseCase 単体テスト - test_note_repository.py Repository 契約テスト - test_async_note_use_case.py 非同期 UseCase テスト + note/ Note domain tests + test_list_notes.py UseCase unit tests + test_note_repository.py Repository contract tests + test_async_note_use_case.py Async UseCase tests comment/ - test_comment_use_case.py UseCase 単体テスト(DB なし) - test_comment_repository.py InMemory + SQLAlchemy の契約テスト - test_comment_http.py HTTP 統合テスト(TestClient) + test_comment_use_case.py UseCase unit tests (no DB) + test_comment_repository.py InMemory + SQLAlchemy contract tests + test_comment_http.py HTTP integration tests (TestClient) ``` -## テストの種類 +## Test types -### UseCase 単体テスト +### UseCase unit tests -DB なし・InMemory リポジトリを使用。最も高速。 +No database, no HTTP — use InMemory repositories. Fastest. ```python def test_create_note() -> None: @@ -48,9 +51,9 @@ def test_create_note() -> None: assert note.title == "t" ``` -### Repository 契約テスト +### Repository contract tests -`@pytest.fixture(params=["inmemory", "sqlalchemy"])` で 2 実装を同一テストで検証。 +`@pytest.fixture(params=["inmemory", "sqlalchemy"])` runs the same assertions against both implementations. ```python @pytest.fixture(params=["inmemory", "sqlalchemy"]) @@ -61,9 +64,9 @@ def test_save_and_find(repo) -> None: assert repo.find_by_id(note.id) == note ``` -### HTTP 統合テスト +### HTTP integration tests -FastAPI `TestClient` 経由。ルーター全体を検証。 +Use FastAPI's `TestClient`. Tests the full stack from HTTP to repository. ```python def test_create_note_returns_201() -> None: @@ -72,9 +75,9 @@ def test_create_note_returns_201() -> None: assert response.status_code == 201 ``` -### 非同期テスト +### Async tests -`asyncio_mode = "auto"` 設定済みのため `async def test_*` がそのまま動きます。 +`asyncio_mode = "auto"` is set in `pyproject.toml`, so `async def test_*` works directly. ```python async def test_async_list_notes() -> None: @@ -83,16 +86,18 @@ async def test_async_list_notes() -> None: assert result.total == 0 ``` -## カバレッジ要件 +## Coverage requirements -- 全体: 80% 以上(CI で強制) -- UseCase / Domain 層: 90% 以上を目標 +| Scope | Target | +|---|---| +| Overall | ≥ 80% (CI enforced) | +| UseCase / Domain | ≥ 90% (goal) | -## 静的解析 +## Static analysis ```bash -uv run mypy src/ # 型チェック -uv run ruff check src/ # リント -uv run ruff format --check src/ tests/ # フォーマットチェック -uv run pip-audit # 依存関係の脆弱性スキャン +uv run mypy src/ # Type checking (strict) +uv run ruff check src/ # Lint +uv run ruff format --check src/ tests/ # Format check +uv run pip-audit # Dependency vulnerability scan ``` diff --git a/docs/howto/mcp-setup.md b/docs/howto/mcp-setup.md index d00c1b8..e072a53 100644 --- a/docs/howto/mcp-setup.md +++ b/docs/howto/mcp-setup.md @@ -1,18 +1,18 @@ -# MCP セットアップガイド — Claude Desktop 連携 +# MCP setup guide — Claude Desktop integration -## 概要 +## Overview -`example/mcp.py` は Note と Tag の全 UseCase (10個) を MCP ツールとして公開する。 -Claude Desktop や `claude` CLI から直接 CRUD 操作が可能になる。 +`example/mcp.py` exposes all Note, Tag, and Comment UseCases (15 tools) as MCP tools. +Once configured, Claude Desktop and the `claude` CLI can perform CRUD operations directly. -## 前提 +## Prerequisites -- `uv sync` 完了済み -- Python 3.12+ 環境 +- `uv sync` completed +- Python 3.12+ environment -## Claude Desktop への設定 +## Claude Desktop configuration -`claude_desktop_config.json` に以下を追加する: +Add the following to `claude_desktop_config.json`: ```json { @@ -25,7 +25,7 @@ Claude Desktop や `claude` CLI から直接 CRUD 操作が可能になる。 "run", "python", "-m", - "example" + "example.mcp" ], "env": { "DB_ADAPTER": "sqlite", @@ -36,37 +36,42 @@ Claude Desktop や `claude` CLI から直接 CRUD 操作が可能になる。 } ``` -`/path/to/nene2-python` をリポジトリの絶対パスに変更すること。 +Replace `/path/to/nene2-python` with the absolute path to this repository. -## claude CLI での起動確認 +## Available tools -```bash -# ファイル永続化 SQLite で起動 -DB_ADAPTER=sqlite DB_NAME=./data/nene2.db uv run python -m example +| Tool | Description | +|---|---| +| `list_notes` | List notes with pagination | +| `get_note` | Get a note by ID | +| `create_note` | Create a new note | +| `update_note` | Update a note | +| `delete_note` | Delete a note | +| `list_tags` | List tags | +| `get_tag` | Get a tag by ID | +| `create_tag` | Create a new tag | +| `update_tag` | Update a tag | +| `delete_tag` | Delete a tag | +| `list_comments` | List comments on a note | +| `get_comment` | Get a comment by ID | +| `create_comment` | Create a comment on a note | +| `update_comment` | Update a comment | +| `delete_comment` | Delete a comment | + +## Running via CLI -# インメモリ SQLite(テスト用) -uv run python -m example +```bash +uv run python -m example.mcp ``` -## 利用可能なツール +The server listens on stdin/stdout (stdio transport) — standard for MCP. -| ツール | 説明 | -|---|---| -| `list_notes(limit, offset)` | Note 一覧取得 | -| `get_note(note_id)` | Note 1件取得 | -| `create_note(title, body)` | Note 作成 | -| `update_note(note_id, title, body)` | Note 更新 | -| `delete_note(note_id)` | Note 削除 | -| `list_tags(limit, offset)` | Tag 一覧取得 | -| `get_tag(tag_id)` | Tag 1件取得 | -| `create_tag(name)` | Tag 作成 | -| `update_tag(tag_id, name)` | Tag 更新 | -| `delete_tag(tag_id)` | Tag 削除 | - -## データディレクトリの準備 +## Custom transport -```bash -mkdir -p data -``` +```python +from example.mcp import create_mcp_server -SQLite ファイルは初回起動時に自動作成される。 +server = create_mcp_server() +server.run(transport="sse") # Server-Sent Events +server.run(transport="streamable-http") # HTTP streaming +``` diff --git a/docs/ja/explanation/architecture.md b/docs/ja/explanation/architecture.md new file mode 100644 index 0000000..b75e099 --- /dev/null +++ b/docs/ja/explanation/architecture.md @@ -0,0 +1,100 @@ +# アーキテクチャ概要 + +## レイヤー構造 + +nene2-python はクリーンアーキテクチャに基づいており、依存関係は外から内へ向かいます。 + +``` +┌─────────────────────────────────────────────┐ +│ HTTP Handler (FastAPI router) │ +│ parse request → call use-case → response │ +├─────────────────────────────────────────────┤ +│ UseCase │ +│ ビジネスロジック。HTTP・DB を知らない │ +├─────────────────────────────────────────────┤ +│ RepositoryInterface (ABC) │ +│ ドメインが必要とする操作の契約 │ +├─────────────────────────────────────────────┤ +│ ConcreteRepository │ +│ SQLAlchemy / InMemory 実装 │ +└─────────────────────────────────────────────┘ +``` + +## 各レイヤーの責務 + +### HTTP Handler + +- **唯一の責務**: リクエストを解析し UseCase を呼び、レスポンスを返す +- Pydantic `BaseModel` でリクエストボディを検証(HTTP 境界のみ) +- ドメインロジックを持たない +- `make_xxx_router()` ファクトリ関数がルーターを返す + +```python +@router.post("", status_code=201) +async def create_note(body: CreateNoteBody) -> JSONResponse: + note = create_use_case.execute(CreateNoteInput(title=body.title, body=body.body)) + return JSONResponse({"id": note.id, "title": note.title, "body": note.body}, status_code=201) +``` + +### UseCase + +- **唯一の責務**: ビジネスルールを実装する +- `execute(input_: XxxInput) -> XxxOutput` の単一メソッド +- `import fastapi`, `import sqlalchemy` を持たない +- 他の UseCase を呼ばない(オーケストレーションは上位層) +- `InMemoryRepository` でテスト可能 + +### RepositoryInterface + +- ABC で契約を定義 +- UseCase は Interface のみに依存する(具象クラスを知らない) +- InMemory 実装と SQLAlchemy 実装が同じ Interface を実装する + +### ConcreteRepository + +- SQLAlchemy Core(ORM なし)でパラメータ化クエリを実行 +- `SqlAlchemyQueryExecutor` でクエリを抽象化 +- テーブルスキーマは `src/example/schema.py` で一元管理 + +## ミドルウェアスタック + +リクエストは外側から内側に向かって処理されます: + +``` +BearerTokenMiddleware 認証 (Bearer Token) +ApiKeyAuthMiddleware 認証 (API Key) +CORSMiddleware CORS +ThrottleMiddleware レートリミット +RequestSizeLimitMiddleware ペイロードサイズ制限 +RequestLoggingMiddleware リクエストロギング +RequestIdMiddleware リクエスト ID 付与 +SecurityHeadersMiddleware セキュリティヘッダー付与 +ErrorHandlerMiddleware 例外 → RFC 9457 Problem Details 変換 +``` + +## DI パターン + +FastAPI の `Depends` は HTTP 境界のみで使用します。UseCase とリポジトリはコンストラクタインジェクションで接続します。 + +```python +# app.py — ワイヤリング +note_repo = SqlAlchemyNoteRepository(executor) +app.include_router(make_note_router( + list_use_case=ListNotesUseCase(note_repo), + create_use_case=CreateNoteUseCase(note_repo), + ... +)) +``` + +## ドメインパッケージ構造 + +``` +src/example// + __init__.py + entity.py — @dataclass(frozen=True, slots=True) + repository.py — ABC + InMemory 実装 + exceptions.py — XxxNotFoundException + ExceptionHandler + use_case.py — 5 UseCase + Input/Output DTO + handler.py — FastAPI router + sqlalchemy_repository.py — SQL バックエンド +``` diff --git a/docs/ja/explanation/design-philosophy.md b/docs/ja/explanation/design-philosophy.md new file mode 100644 index 0000000..4bcb0f1 --- /dev/null +++ b/docs/ja/explanation/design-philosophy.md @@ -0,0 +1,68 @@ +# 設計思想と PHP NENE2 との対応 + +## NENE2 の設計原則 + +nene2-python は PHP 版 NENE2 と同一の設計思想を持ちます。 + +### API First + +JSON API と OpenAPI 契約を中心に据えます。DB 設計より先に API の形を定義し、スキーマを `uv run export-openapi` で生成します。 + +### 薄い HTTP 層 + +HTTP Handler はビジネスロジックを持ちません。**parse → use-case → response** の 3 ステップのみ。ドメインルールは UseCase に集約されます。 + +### AI-readable + +明示的なディレクトリ構造、小さなクラス(150 行以下)、型付き境界により、LLM がコードベースを正確に理解・操作できます。 + +### Security First + +セキュリティは後付けではなく設計の出発点です。 +- Pydantic による HTTP 境界の全入力検証 +- パラメータ化クエリのみ(SQLインジェクション防止) +- `secrets.compare_digest` によるタイミング安全な比較 +- セキュリティヘッダーをミドルウェアで付与 + +### LLM Delivery Ready + +UseCase は HTTP・DB から独立しているため、MCP ツールとして直接再利用できます。`src/example/mcp.py` はその実証です。 + +## PHP NENE2 との対応表 + +| PHP 版 | Python 版 | 備考 | +|---|---|---| +| `readonly class` | `@dataclass(frozen=True, slots=True)` | 不変 Value Object | +| `ValidationException` + `ValidationError` | 同名クラス (`nene2.validation`) | 422 + Problem Details | +| `PaginationQueryParser` | `nene2.http.PaginationQueryParser` | クエリパラメータ解析 | +| `PaginationResponse` | `nene2.http.PaginationResponse` | ページネーションレスポンス | +| `ProblemDetailsResponseFactory` | `nene2.http.problem_details_response()` | RFC 9457 | +| `ErrorHandlerMiddleware` | `nene2.middleware.ErrorHandlerMiddleware` | 全例外をキャッチ | +| `PHPStan level 8` | `mypy --strict` | 最高レベルの型チェック | +| `PHP-CS-Fixer` | `ruff format` | コードフォーマット | +| `UseCaseInterface` | `nene2.use_case.UseCaseProtocol[I, O]` | 構造的サブタイピング | + +## Python 3.12+ 固有の選択 + +| 用途 | 選択 | 理由 | +|---|---|---| +| 型エイリアス | `type X = list[str]` | PEP 695 — 新構文 | +| ジェネリクス | `class Foo[T]` | PEP 695 — TypeVar 不要 | +| 不変 VO | `dataclass(frozen=True, slots=True)` | メモリ効率 + 不変性 | +| HTTP 検証 | Pydantic v2 BaseModel | 高速 + 型安全 | +| SQL | SQLAlchemy Core | ORM なしで SQL を直接制御 | +| ロギング | structlog | JSON / Console の両対応 | +| MCP | mcp (Anthropic SDK) | FastMCP ラッパー | + +## ADR 一覧 + +設計の個別決定は ADR に記録されています: + +- [ADR-0001: ツールチェーン](../adr/0001-toolchain.md) +- [ADR-0002: クリーンアーキテクチャ](../adr/0002-clean-architecture.md) +- [ADR-0003: セキュリティファースト](../adr/0003-security-first.md) +- [ADR-0004: AI ファースト設計](../adr/0004-ai-first-design.md) +- [ADR-0005: ロギング](../adr/0005-logging.md) +- [ADR-0006: レートリミット](../adr/0006-rate-limiting.md) +- [ADR-0009: MCP 設計](../adr/0009-mcp-design.md) +- [ADR-0010: AsyncUseCase パターン](../adr/0010-async-use-case.md) diff --git a/docs/ja/how-to/add-new-domain.md b/docs/ja/how-to/add-new-domain.md new file mode 100644 index 0000000..d866cd5 --- /dev/null +++ b/docs/ja/how-to/add-new-domain.md @@ -0,0 +1,87 @@ +# 新しいドメインを追加する + +既存の Note・Tag・Comment ドメインと同じパターンで新しいドメインを追加するチェックリストです。 + +## チェックリスト + +### 1. ドメインパッケージを作成する + +```bash +mkdir -p src/example/ +touch src/example//__init__.py +``` + +### 2. 各ファイルを作成する + +| ファイル | 内容 | +|---|---| +| `entity.py` | `@dataclass(frozen=True, slots=True)` でエンティティを定義 | +| `repository.py` | `XxxRepositoryInterface(ABC)` + `InMemoryXxxRepository` | +| `exceptions.py` | `XxxNotFoundException` + `XxxNotFoundExceptionHandler` | +| `use_case.py` | 5 UseCase (List / Get / Create / Update / Delete) + Input/Output DTO | +| `handler.py` | `make_xxx_router()` — parse → use-case → response | +| `sqlalchemy_repository.py` | SQL バックエンド実装 | + +### 3. schema.py にテーブルを追加する + +`src/example/schema.py` の `ensure_schema()` にテーブル定義を追加します。 + +```python +executor.write( + "CREATE TABLE IF NOT EXISTS your_domain (" + "id INTEGER PRIMARY KEY AUTOINCREMENT," + "name TEXT NOT NULL," + "created_at DATETIME DEFAULT CURRENT_TIMESTAMP" + ")" +) +``` + +### 4. app.py に組み込む + +`src/example/app.py` の `_build_repositories()` と `create_app()` を更新します。 + +```python +# _build_repositories() の戻り値に追加 +your_repo = SqlAlchemyYourRepository(executor) + +# create_app() でルーターを登録 +app.include_router(make_your_router( + list_use_case=ListYourUseCase(your_repo), + ... +)) +``` + +### 5. テストを書く + +``` +tests/example// + __init__.py + test__use_case.py # UseCase 単体テスト(DB なし) + test__repository.py # Repository 契約テスト(InMemory + SQLAlchemy) + test__http.py # HTTP 統合テスト(TestClient) +``` + +### 6. MCP ツールに追加する(任意) + +`src/example/mcp.py` の `create_mcp_server()` に UseCase を登録します。 + +### 7. 全チェックを通過させる + +```bash +uv run pytest && \ +uv run mypy src/ && \ +uv run ruff check src/ tests/ && \ +uv run ruff format --check src/ tests/ +``` + +## 命名規則 + +- エンティティクラス: `PascalCase` (`Note`, `Tag`, `Comment`) +- UseCase 入力 DTO: `XxxInput` (`CreateNoteInput`) +- 例外: `XxxNotFoundException` +- ハンドラーファクトリ: `make_xxx_router()` + +## 参考実装 + +- `src/example/note/` — 基本的な CRUD ドメイン +- `src/example/comment/` — 外部キー (note_id) を持つネストドメイン diff --git a/docs/ja/how-to/configure-auth.md b/docs/ja/how-to/configure-auth.md new file mode 100644 index 0000000..7d99b8f --- /dev/null +++ b/docs/ja/how-to/configure-auth.md @@ -0,0 +1,101 @@ +# 認証を設定する + +nene2-python は Bearer Token 認証と API Key 認証の 2 種類をサポートしています。 +どちらもミドルウェアとして実装されており、環境変数で有効化できます。 + +## Bearer Token 認証 + +### 有効化する + +`.env` ファイルに以下を追加します: + +```dotenv +BEARER_TOKEN_ENABLED=true +BEARER_TOKENS=token1,token2,token3 +``` + +### 動作 + +- `Authorization: Bearer ` ヘッダーが必須になります +- トークンは `secrets.compare_digest` でタイミング安全な比較を行います +- 無効なトークンは `401 Unauthorized` を返します + +### curl での利用 + +```bash +curl -H "Authorization: Bearer token1" http://localhost:8080/notes +``` + +## API Key 認証 + +### 有効化する + +```dotenv +API_KEY_ENABLED=true +API_KEYS=key1,key2 +``` + +### 動作 + +- `X-Api-Key: ` ヘッダーが必須になります +- 無効なキーは `401 Unauthorized` を返します + +### curl での利用 + +```bash +curl -H "X-Api-Key: key1" http://localhost:8080/notes +``` + +## 両方を有効化する場合 + +Bearer Token と API Key を同時に有効化すると、リクエストは両方の認証を通過する必要があります。 +通常はどちらか一方を使います。 + +## テスト時に認証を無効化する + +```python +from nene2.config import AppSettings +from fastapi.testclient import TestClient +from example.app import create_app + +client = TestClient(create_app(AppSettings(bearer_token_enabled=False))) +``` + +## カスタム TokenVerifier を実装する + +`TokenVerifierProtocol` を実装することで、JWT や外部サービスによる検証を追加できます。 + +```python +from nene2.auth.interfaces import TokenVerifierProtocol +from nene2.auth.exceptions import TokenVerificationException +import jwt + +class JwtTokenVerifier: + def __init__(self, secret: str) -> None: + self._secret = secret + + def verify(self, token: str) -> bool: + try: + jwt.decode(token, self._secret, algorithms=["HS256"]) + return True + except jwt.InvalidTokenError as exc: + raise TokenVerificationException(str(exc)) from exc +``` + +`TokenVerificationException` を raise することで、`BearerTokenMiddleware` が自動的に `401 Unauthorized` を返します。 + +## カスタム TokenIssuer を実装する + +`TokenIssuerProtocol` を実装して、JWT などのトークンを発行できます。 + +```python +from nene2.auth.interfaces import TokenIssuerProtocol +import jwt + +class JwtTokenIssuer: + def __init__(self, secret: str) -> None: + self._secret = secret + + def issue(self, claims: dict[str, object]) -> str: + return jwt.encode(claims, self._secret, algorithm="HS256") +``` diff --git a/docs/ja/how-to/run-tests.md b/docs/ja/how-to/run-tests.md new file mode 100644 index 0000000..a079759 --- /dev/null +++ b/docs/ja/how-to/run-tests.md @@ -0,0 +1,98 @@ +# テストを実行する + +## 基本コマンド + +```bash +# 全テストを実行(カバレッジ付き) +uv run pytest + +# 失敗時の詳細表示 +uv run pytest --tb=short -v + +# 特定のファイルだけ実行 +uv run pytest tests/example/note/ + +# カバレッジ HTML レポートを生成 +uv run pytest --cov=src --cov-report=html +# → htmlcov/index.html をブラウザで開く +``` + +## テスト構造 + +``` +tests/ + nene2/ フレームワークコアの単体テスト + use_case/ UseCaseProtocol 準拠テスト + ... + example/ + note/ Note ドメインテスト + test_list_notes.py UseCase 単体テスト + test_note_repository.py Repository 契約テスト + test_async_note_use_case.py 非同期 UseCase テスト + comment/ + test_comment_use_case.py UseCase 単体テスト(DB なし) + test_comment_repository.py InMemory + SQLAlchemy の契約テスト + test_comment_http.py HTTP 統合テスト(TestClient) +``` + +## テストの種類 + +### UseCase 単体テスト + +DB なし・InMemory リポジトリを使用。最も高速。 + +```python +def test_create_note() -> None: + repo = InMemoryNoteRepository() + note = CreateNoteUseCase(repo).execute(CreateNoteInput(title="t", body="b")) + assert note.title == "t" +``` + +### Repository 契約テスト + +`@pytest.fixture(params=["inmemory", "sqlalchemy"])` で 2 実装を同一テストで検証。 + +```python +@pytest.fixture(params=["inmemory", "sqlalchemy"]) +def repo(request): ... + +def test_save_and_find(repo) -> None: + note = repo.save("title", "body") + assert repo.find_by_id(note.id) == note +``` + +### HTTP 統合テスト + +FastAPI `TestClient` 経由。ルーター全体を検証。 + +```python +def test_create_note_returns_201() -> None: + client = TestClient(create_app(AppSettings(throttle_enabled=False))) + response = client.post("/notes", json={"title": "t", "body": "b"}) + assert response.status_code == 201 +``` + +### 非同期テスト + +`asyncio_mode = "auto"` 設定済みのため `async def test_*` がそのまま動きます。 + +```python +async def test_async_list_notes() -> None: + repo = InMemoryNoteRepository() + result = await AsyncListNotesUseCase(repo).execute(ListNotesInput(limit=10, offset=0)) + assert result.total == 0 +``` + +## カバレッジ要件 + +- 全体: 80% 以上(CI で強制) +- UseCase / Domain 層: 90% 以上を目標 + +## 静的解析 + +```bash +uv run mypy src/ # 型チェック +uv run ruff check src/ # リント +uv run ruff format --check src/ tests/ # フォーマットチェック +uv run pip-audit # 依存関係の脆弱性スキャン +``` diff --git a/docs/ja/howto/mcp-setup.md b/docs/ja/howto/mcp-setup.md new file mode 100644 index 0000000..77c2d19 --- /dev/null +++ b/docs/ja/howto/mcp-setup.md @@ -0,0 +1,77 @@ +# MCP セットアップガイド — Claude Desktop 連携 + +## 概要 + +`example/mcp.py` は Note・Tag・Comment の全 UseCase(15 ツール)を MCP ツールとして公開します。 +Claude Desktop や `claude` CLI から直接 CRUD 操作が可能になります。 + +## 前提 + +- `uv sync` 完了済み +- Python 3.12+ 環境 + +## Claude Desktop への設定 + +`claude_desktop_config.json` に以下を追加します: + +```json +{ + "mcpServers": { + "nene2-example": { + "command": "uv", + "args": [ + "--directory", + "/path/to/nene2-python", + "run", + "python", + "-m", + "example.mcp" + ], + "env": { + "DB_ADAPTER": "sqlite", + "DB_NAME": "/path/to/nene2-python/data/nene2.db" + } + } + } +} +``` + +`/path/to/nene2-python` をリポジトリの絶対パスに変更してください。 + +## 利用可能なツール + +| ツール | 説明 | +|---|---| +| `list_notes` | Note 一覧取得(ページネーション付き) | +| `get_note` | ID 指定で Note を取得 | +| `create_note` | Note を作成 | +| `update_note` | Note を更新 | +| `delete_note` | Note を削除 | +| `list_tags` | Tag 一覧取得 | +| `get_tag` | ID 指定で Tag を取得 | +| `create_tag` | Tag を作成 | +| `update_tag` | Tag を更新 | +| `delete_tag` | Tag を削除 | +| `list_comments` | Note に紐づくコメント一覧 | +| `get_comment` | ID 指定でコメントを取得 | +| `create_comment` | Note にコメントを作成 | +| `update_comment` | コメントを更新 | +| `delete_comment` | コメントを削除 | + +## CLI での起動 + +```bash +uv run python -m example.mcp +``` + +サーバーは stdin/stdout(stdio トランスポート)でリッスンします — MCP の標準形式。 + +## カスタムトランスポート + +```python +from example.mcp import create_mcp_server + +server = create_mcp_server() +server.run(transport="sse") # Server-Sent Events +server.run(transport="streamable-http") # HTTP ストリーミング +``` diff --git a/docs/ja/index.md b/docs/ja/index.md new file mode 100644 index 0000000..779f23f --- /dev/null +++ b/docs/ja/index.md @@ -0,0 +1,43 @@ +--- +layout: home + +hero: + name: "NENE2" + text: "Python API フレームワーク" + tagline: FastAPI · クリーンアーキテクチャ · MCP · mypy --strict · AI ファースト設計 + actions: + - theme: brand + text: はじめる → + link: /ja/tutorials/getting-started + - theme: alt + text: GitHub で見る + link: https://github.com/hideyukiMORI/nene2-python + - theme: alt + text: PHP 版 + link: https://hideyukimori.github.io/NENE2/ + +features: + - icon: 🐍 + title: Python 3.12+ ネイティブ + details: Python 3.12 のジェネリクス構文、frozen dataclass、Pydantic v2 を使用。後方互換 shim なし。mypy --strict を全コミットで強制。 + + - icon: ⚡ + title: FastAPI + async 対応 + details: ASGI ネイティブ。AsyncUseCaseProtocol で非同期 I/O をサポート。asyncio.gather による並列リポジトリ呼び出し。 + + - icon: 🤖 + title: MCP 内蔵 + details: UseCase を LocalMcpServer 経由で MCP ツールとして公開 — 追加配線不要。Claude と任意の MCP クライアントが API を直接呼び出せる。 + + - icon: 🏛️ + title: クリーンアーキテクチャ + details: HTTP Handler → UseCase → RepositoryInterface → SQLAlchemy。各レイヤーを InMemory リポジトリで独立してテスト可能。 + + - icon: 🛡️ + title: セキュリティファースト + details: RFC 9457 Problem Details、Bearer + API Key 認証、レートリミット、セキュリティヘッダー、リクエストサイズ制限 — すべて即使用可能。 + + - icon: 📄 + title: OpenAPI 自動生成 + details: Swagger UI と ReDoc を /docs で提供 — 設定ゼロ。1コマンドで静的 openapi.yaml をエクスポート。 +--- diff --git a/docs/ja/reference/api.md b/docs/ja/reference/api.md new file mode 100644 index 0000000..d461e89 --- /dev/null +++ b/docs/ja/reference/api.md @@ -0,0 +1,137 @@ +# REST API リファレンス + +nene2-python example アプリが提供するエンドポイントの一覧です。 + +> OpenAPI スキーマ(機械可読)は `uv run export-openapi` で `docs/openapi.yaml` に生成できます。 +> 開発サーバー起動後は `http://localhost:8080/docs` で Swagger UI を参照できます。 + +--- + +## Notes + +### `GET /notes` + +ノート一覧を取得します。 + +**クエリパラメータ** + +| パラメータ | 型 | デフォルト | 説明 | +|---|---|---|---| +| `limit` | int | 20 | 最大取得件数(上限 100) | +| `offset` | int | 0 | スキップ件数 | + +**レスポンス** `200 OK` + +```json +{ + "items": [{"id": 1, "title": "ノートタイトル", "body": "本文"}], + "limit": 20, + "offset": 0, + "total": 1 +} +``` + +### `POST /notes` + +ノートを作成します。 + +**リクエストボディ** + +```json +{"title": "タイトル", "body": "本文"} +``` + +**レスポンス** `201 Created` + +```json +{"id": 1, "title": "タイトル", "body": "本文"} +``` + +### `GET /notes/{note_id}` + +指定した ID のノートを取得します。存在しない場合は `404` を返します。 + +### `PUT /notes/{note_id}` + +ノートを更新します。 + +**リクエストボディ** + +```json +{"title": "新タイトル", "body": "新本文"} +``` + +**レスポンス** `200 OK` / `404 Not Found` + +### `DELETE /notes/{note_id}` + +ノートを削除します。`204 No Content` / `404 Not Found` を返します。 + +--- + +## Tags + +`/tags` エンドポイントは Notes と同じ CRUD パターンです。 + +| メソッド | パス | 説明 | +|---|---|---| +| `GET` | `/tags` | タグ一覧 | +| `POST` | `/tags` | タグ作成(`{"name": "..."}`) | +| `GET` | `/tags/{tag_id}` | タグ取得 | +| `PUT` | `/tags/{tag_id}` | タグ更新(`{"name": "..."}`) | +| `DELETE` | `/tags/{tag_id}` | タグ削除 | + +--- + +## Comments + +コメントはノートに紐づくネストリソースです。 + +| メソッド | パス | 説明 | +|---|---|---| +| `GET` | `/notes/{note_id}/comments` | コメント一覧 | +| `POST` | `/notes/{note_id}/comments` | コメント作成(`{"body": "..."}`) | +| `GET` | `/notes/{note_id}/comments/{comment_id}` | コメント取得 | +| `PUT` | `/notes/{note_id}/comments/{comment_id}` | コメント更新 | +| `DELETE` | `/notes/{note_id}/comments/{comment_id}` | コメント削除 | + +--- + +## Health Check + +### `GET /health` + +アプリケーションの稼働状態を返します。 + +**レスポンス** `200 OK` + +```json +{"status": "ok", "db": "ok"} +``` + +DB 接続失敗時は `db` フィールドが `"error"` になります。 + +--- + +## エラーレスポンス + +すべてのエラーは RFC 9457 Problem Details 形式で返します。 + +```json +{ + "type": "https://nene2.dev/problems/not-found", + "title": "Not Found", + "status": 404, + "detail": "Note with ID 42 was not found." +} +``` + +| ステータス | type | 原因 | +|---|---|---| +| 400 | `bad-request` | 不正なリクエスト | +| 401 | `unauthorized` | 認証失敗 | +| 404 | `not-found` | リソースが存在しない | +| 413 | `payload-too-large` | ペイロードサイズ超過 | +| 422 | `validation-failed` | バリデーションエラー | +| 429 | `too-many-requests` | レートリミット超過 | +| 500 | `internal-server-error` | サーバー内部エラー | diff --git a/docs/ja/reference/configuration.md b/docs/ja/reference/configuration.md new file mode 100644 index 0000000..49a402e --- /dev/null +++ b/docs/ja/reference/configuration.md @@ -0,0 +1,105 @@ +# 設定リファレンス(環境変数) + +設定は `pydantic-settings` で管理されており、環境変数または `.env` ファイルから読み込みます。 + +## 基本設定 + +| 変数 | デフォルト | 説明 | +|---|---|---| +| `APP_ENV` | `local` | 実行環境。`local` / `test` / `production` | +| `APP_DEBUG` | `false` | `true` の場合、500 エラーに例外メッセージを含める | +| `APP_NAME` | `nene2-python` | アプリケーション名 | + +## セキュリティ設定 + +| 変数 | デフォルト | 説明 | +|---|---|---| +| `SECURITY_HEADERS_ENABLED` | `true` | セキュリティヘッダー付与を有効化 | +| `MAX_BODY_SIZE` | `1048576` | リクエストボディの最大バイト数(デフォルト 1 MiB) | + +セキュリティヘッダーの内容: + +| ヘッダー | 値 | +|---|---| +| `X-Content-Type-Options` | `nosniff` | +| `X-Frame-Options` | `DENY` | +| `Referrer-Policy` | `strict-origin-when-cross-origin` | +| `Content-Security-Policy` | `default-src 'self'` | +| `Permissions-Policy` | `geolocation=(), microphone=()` | + +## レートリミット + +| 変数 | デフォルト | 説明 | +|---|---|---| +| `THROTTLE_ENABLED` | `true` | レートリミットを有効化 | +| `THROTTLE_LIMIT` | `60` | ウィンドウ内の最大リクエスト数 | +| `THROTTLE_WINDOW` | `60` | ウィンドウの秒数 | + +固定ウィンドウ方式(IP アドレスをキーとする)。制限超過時は `429 Too Many Requests` + `Retry-After` ヘッダー。 + +## CORS 設定 + +| 変数 | デフォルト | 説明 | +|---|---|---| +| `CORS_ENABLED` | `false` | CORS ミドルウェアを有効化 | +| `CORS_ORIGINS` | `[]` | 許可オリジンのリスト(カンマ区切り) | +| `CORS_ALLOW_CREDENTIALS` | `false` | クレデンシャルを許可するか | +| `CORS_ALLOW_METHODS` | `GET,POST,PUT,DELETE,OPTIONS` | 許可メソッド | +| `CORS_ALLOW_HEADERS` | `*` | 許可ヘッダー | + +> `CORS_ORIGINS=*` は禁止です。許可オリジンを明示してください。 + +## 認証設定 + +| 変数 | デフォルト | 説明 | +|---|---|---| +| `BEARER_TOKEN_ENABLED` | `false` | Bearer Token 認証を有効化 | +| `BEARER_TOKENS` | `[]` | 有効なトークンのリスト(カンマ区切り) | +| `API_KEY_ENABLED` | `false` | API Key 認証を有効化 | +| `API_KEYS` | `[]` | 有効な API キーのリスト(カンマ区切り) | + +## データベース設定 + +| 変数 | デフォルト | 説明 | +|---|---|---| +| `DB_ADAPTER` | `sqlite` | `sqlite` / `mysql` / `pgsql` | +| `DB_NAME` | `:memory:` | SQLite のファイルパスまたは DB 名 | +| `DB_HOST` | `localhost` | DB ホスト(SQLite では無視) | +| `DB_PORT` | `3306` | DB ポート(SQLite では無視) | +| `DB_USER` | `""` | DB ユーザー名(SQLite では無視) | +| `DB_PASSWORD` | `""` | DB パスワード — `SecretStr` 型(ログに出力されない) | + +### 接続 URL の例 + +| アダプター | 生成される URL | +|---|---| +| `sqlite` | `sqlite:///path/to/db.sqlite3` | +| `mysql` | `mysql+pymysql://user:pass@host:3306/dbname` | +| `pgsql` | `postgresql+psycopg2://user:pass@host:5432/dbname` | + +## .env ファイル例 + +```dotenv +APP_ENV=production +APP_DEBUG=false +APP_NAME=my-api + +THROTTLE_ENABLED=true +THROTTLE_LIMIT=100 +THROTTLE_WINDOW=60 + +CORS_ENABLED=true +CORS_ORIGINS=https://example.com,https://app.example.com + +BEARER_TOKEN_ENABLED=true +BEARER_TOKENS=secret-token-1,secret-token-2 + +DB_ADAPTER=mysql +DB_HOST=db.example.com +DB_PORT=3306 +DB_NAME=myapp +DB_USER=myuser +DB_PASSWORD=supersecret +``` + +> `.env` ファイルは `.gitignore` で除外してください。`.env.example` にキー一覧をコミットしてください。 diff --git a/docs/ja/reference/framework-modules.md b/docs/ja/reference/framework-modules.md new file mode 100644 index 0000000..195a383 --- /dev/null +++ b/docs/ja/reference/framework-modules.md @@ -0,0 +1,224 @@ +# フレームワークモジュールリファレンス + +`src/nene2/` パッケージが提供するコアモジュールの一覧です。 + +--- + +## nene2.http + +### `PaginationQueryParser` + +クエリパラメータ `limit` と `offset` を解析します。 + +```python +from nene2.http import PaginationQueryParser + +pagination = PaginationQueryParser.parse(request) +# pagination.limit → int (max 100, default 20) +# pagination.offset → int (default 0) +``` + +### `PaginationResponse` + +ページネーションレスポンスの構造。 + +```python +from nene2.http import PaginationResponse + +body = PaginationResponse(items=[...], limit=20, offset=0, total=42).to_dict() +# → {"items": [...], "limit": 20, "offset": 0, "total": 42} +``` + +### `problem_details_response()` + +RFC 9457 準拠のエラーレスポンスを生成します。 + +```python +from nene2.http import problem_details_response + +return problem_details_response("not-found", "Not Found", 404, "Note 42 not found.") +``` + +--- + +## nene2.use_case + +### `UseCaseProtocol[I, O]` + +同期 UseCase の構造的型契約。 + +```python +from nene2.use_case import UseCaseProtocol + +class MyUseCase: + def execute(self, input_: MyInput) -> MyOutput: ... + +assert isinstance(MyUseCase(), UseCaseProtocol) +``` + +### `AsyncUseCaseProtocol[I, O]` + +非同期 UseCase の構造的型契約。 + +```python +from nene2.use_case import AsyncUseCaseProtocol + +class MyAsyncUseCase: + async def execute(self, input_: MyInput) -> MyOutput: ... + +assert isinstance(MyAsyncUseCase(), AsyncUseCaseProtocol) +``` + +> **注意**: `isinstance` はメソッドの存在のみを確認します。同期/非同期の区別は `mypy --strict` で静的に強制されます。 + +--- + +## nene2.config + +### `AppSettings` + +環境変数から設定を読み込む Pydantic Settings クラス。 + +```python +from nene2.config import AppSettings + +cfg = AppSettings() # 環境変数 / .env から読み込み +cfg_test = AppSettings(throttle_enabled=False) # テスト用オーバーライド +``` + +詳細は [設定リファレンス](configuration.md) を参照してください。 + +--- + +## nene2.middleware + +### `ErrorHandlerMiddleware` + +全例外をキャッチし RFC 9457 Problem Details に変換します。 +ドメイン例外ハンドラーは `DomainExceptionHandlerProtocol` を実装して登録します。 + +### その他のミドルウェア + +| クラス | モジュール | 役割 | +|---|---|---| +| `SecurityHeadersMiddleware` | `nene2.middleware.security_headers` | セキュリティヘッダー付与 | +| `RequestIdMiddleware` | `nene2.middleware.request_id` | X-Request-ID 付与 | +| `RequestLoggingMiddleware` | `nene2.middleware.request_logging` | structlog リクエストロギング | +| `RequestSizeLimitMiddleware` | `nene2.middleware.request_size_limit` | ペイロードサイズ制限 | +| `ThrottleMiddleware` | `nene2.middleware.throttle` | 固定ウィンドウ レートリミット | + +--- + +## nene2.auth + +### `LocalTokenVerifier` + +`secrets.compare_digest` で静的トークンリストを検証します。 + +```python +from nene2.auth import LocalTokenVerifier + +verifier = LocalTokenVerifier(["token-a", "token-b"]) +verifier.verify("token-a") # True +verifier.verify("wrong") # False +``` + +### `TokenVerifierProtocol` / `TokenIssuerProtocol` + +カスタム検証器・発行器の実装に使うプロトコル(JWT など)。 + +### `TokenVerificationException` + +トークンが無効な場合にverifier から raise します。 +`BearerTokenMiddleware` が自動的に `401 Unauthorized` に変換します。 + +--- + +## nene2.database + +### `SqlAlchemyQueryExecutor` + +SQLAlchemy Core のラッパー。パラメータ化クエリを実行します。 + +```python +from nene2.database import SqlAlchemyQueryExecutor + +executor = SqlAlchemyQueryExecutor(engine) +rows = executor.fetch_all("SELECT * FROM notes WHERE id = :id", {"id": 1}) +executor.write("INSERT INTO notes (title, body) VALUES (:t, :b)", {"t": "t", "b": "b"}) +``` + +### `SqlAlchemyTransactionManager` + +トランザクションを管理します。手動の `begin/commit/rollback` より `transactional()` を推奨します。 + +```python +from nene2.database import SqlAlchemyTransactionManager + +mgr = SqlAlchemyTransactionManager(engine) + +result = mgr.transactional( + lambda ex: ex.fetch_one("SELECT COUNT(*) AS cnt FROM notes") +) +``` + +--- + +## nene2.mcp + +### `LocalMcpServer` + +FastMCP をラップして UseCase を MCP ツールとして登録します。 + +```python +from nene2.mcp import LocalMcpServer + +server = LocalMcpServer("my-server", instructions="...") + +@server.tool("ノート一覧を取得する。") +def list_notes(limit: int = 20, offset: int = 0) -> list[dict]: ... + +server.run(transport="stdio") +``` + +### `HttpxMcpClient` + +MCP ツールハンドラーから nene2 API を呼び出す HTTP クライアント。 + +```python +from nene2.mcp import HttpxMcpClient + +client = HttpxMcpClient("bearer-token") +response = client.get("http://localhost:8080", "/notes") +response.is_successful() # True +``` + +--- + +## nene2.log + +### `setup_logging()` + +structlog を初期化します。`app_env` に応じてレンダラーを切り替えます。 + +```python +from nene2.log import setup_logging + +setup_logging(app_env="production") # JSON レンダラー +setup_logging(app_env="local") # Console レンダラー +``` + +--- + +## nene2.validation + +### `ValidationException` / `ValidationError` + +HTTP 入力検証失敗時に `422 Unprocessable Entity` を返す例外。 + +```python +from nene2.validation.exceptions import ValidationError, ValidationException + +errors = [ValidationError("body", "Body must not be empty.", "required")] +raise ValidationException(errors) +``` diff --git a/docs/ja/tutorials/first-domain.md b/docs/ja/tutorials/first-domain.md new file mode 100644 index 0000000..d64275c --- /dev/null +++ b/docs/ja/tutorials/first-domain.md @@ -0,0 +1,154 @@ +# 新しいドメインを実装する + +このチュートリアルでは、`Tag` ドメインを例に nene2-python のクリーンアーキテクチャを体験します。 +各レイヤーを順番に実装することで、フレームワークの構造全体を理解できます。 + +> **前提**: [はじめての nene2-python](getting-started.md) を完了していること + +## 実装するもの + +``` +GET /tags — タグ一覧 +POST /tags — タグ作成 +GET /tags/{tag_id} — タグ取得 +PUT /tags/{tag_id} — タグ更新 +DELETE /tags/{tag_id} — タグ削除 +``` + +## ステップ 1: Entity を作る + +`src/example/tag/entity.py` を作成します。 + +```python +from dataclasses import dataclass + +@dataclass(frozen=True, slots=True) +class Tag: + id: int + name: str +``` + +`frozen=True` で不変オブジェクト、`slots=True` でメモリ効率を高めています。 + +## ステップ 2: Repository Interface を作る + +`src/example/tag/repository.py` に ABC を定義します。 + +```python +from abc import ABC, abstractmethod +from .entity import Tag + +class TagRepositoryInterface(ABC): + @abstractmethod + def find_all(self, limit: int, offset: int) -> list[Tag]: ... + + @abstractmethod + def find_by_id(self, tag_id: int) -> Tag | None: ... + + @abstractmethod + def save(self, name: str) -> Tag: ... + + @abstractmethod + def update(self, tag_id: int, name: str) -> Tag | None: ... + + @abstractmethod + def delete(self, tag_id: int) -> bool: ... + + @abstractmethod + def count(self) -> int: ... +``` + +## ステップ 3: InMemory 実装を作る + +テスト用の InMemory リポジトリを実装します。 + +```python +class InMemoryTagRepository(TagRepositoryInterface): + def __init__(self) -> None: + self._store: dict[int, Tag] = {} + self._next_id = 1 + + def save(self, name: str) -> Tag: + tag = Tag(id=self._next_id, name=name) + self._store[self._next_id] = tag + self._next_id += 1 + return tag + # ... 省略 +``` + +## ステップ 4: UseCase を作る + +`src/example/tag/use_case.py` に UseCase を定義します。UseCase は HTTP・DB を知りません。 + +```python +from dataclasses import dataclass +from .entity import Tag +from .exceptions import TagNotFoundException +from .repository import TagRepositoryInterface + +@dataclass(frozen=True) +class CreateTagInput: + name: str + +class CreateTagUseCase: + def __init__(self, repository: TagRepositoryInterface) -> None: + self._repository = repository + + def execute(self, input_: CreateTagInput) -> Tag: + return self._repository.save(input_.name) +``` + +## ステップ 5: Handler を作る + +`src/example/tag/handler.py` に HTTP ルーターを定義します。**parse → use-case → response** の 3 ステップだけ。 + +```python +from fastapi import APIRouter +from fastapi.responses import JSONResponse +from pydantic import BaseModel +from .use_case import CreateTagInput, CreateTagUseCase + +class CreateTagBody(BaseModel): + name: str + +def make_tag_router(create_use_case: CreateTagUseCase, ...) -> APIRouter: + router = APIRouter(prefix="/tags", tags=["tags"]) + + @router.post("", status_code=201) + async def create_tag(body: CreateTagBody) -> JSONResponse: + tag = create_use_case.execute(CreateTagInput(name=body.name)) + return JSONResponse({"id": tag.id, "name": tag.name}, status_code=201) + + return router +``` + +## ステップ 6: app.py に組み込む + +`src/example/app.py` の `create_app()` にルーターを追加します。 + +```python +app.include_router(make_tag_router( + list_use_case=ListTagsUseCase(tag_repo), + ... +)) +``` + +## ステップ 7: テストを書く + +```python +# tests/example/tag/test_tag_use_case.py +def test_create_tag() -> None: + repo = InMemoryTagRepository() + tag = CreateTagUseCase(repo).execute(CreateTagInput(name="python")) + assert tag.name == "python" +``` + +## 完了 + +実装した Tag ドメインは Comment ドメイン (`src/example/comment/`) と同じ構造です。 +実際の実装は `src/example/tag/` を参照してください。 + +## 次のステップ + +- [新しいドメインを追加する](../how-to/add-new-domain.md) — チェックリスト形式の実践ガイド +- [アーキテクチャ概要](../explanation/architecture.md) — 各レイヤーの役割を深く理解する diff --git a/docs/ja/tutorials/getting-started.md b/docs/ja/tutorials/getting-started.md new file mode 100644 index 0000000..ce903a2 --- /dev/null +++ b/docs/ja/tutorials/getting-started.md @@ -0,0 +1,55 @@ +# はじめての nene2-python + +このチュートリアルでは、nene2-python を使って Note の CRUD API を 5 分で起動します。 + +## 前提条件 + +- Python 3.12 以上 +- [uv](https://docs.astral.sh/uv/) がインストール済み +- Git + +## 1. リポジトリを clone する + +```bash +git clone https://github.com/hideyukiMORI/nene2-python.git +cd nene2-python +``` + +## 2. 依存関係をインストールする + +```bash +uv sync +``` + +## 3. 開発サーバーを起動する + +```bash +uv run uvicorn src.example.app:app --reload --port 8080 +``` + +起動後、ブラウザで `http://localhost:8080/docs` を開くと Swagger UI が表示されます。 + +## 4. API を試す + +```bash +# Note を作成する +curl -X POST http://localhost:8080/notes \ + -H "Content-Type: application/json" \ + -d '{"title": "はじめてのノート", "body": "nene2-python で作成しました"}' + +# Note 一覧を取得する +curl http://localhost:8080/notes +``` + +## 5. テストを実行する + +```bash +uv run pytest +``` + +135 件以上のテストがすべて通ることを確認してください。 + +## 次のステップ + +- [新しいドメインを実装する](first-domain.md) — Tag ドメインの実装を通じてフレームワークの構造を理解する +- [設定リファレンス](../reference/configuration.md) — 環境変数で DB や認証を設定する diff --git a/docs/pt-br/index.md b/docs/pt-br/index.md new file mode 100644 index 0000000..fd4fb29 --- /dev/null +++ b/docs/pt-br/index.md @@ -0,0 +1,43 @@ +--- +layout: home + +hero: + name: "NENE2" + text: "Framework de API Python" + tagline: FastAPI · Arquitetura Limpa · MCP · mypy --strict · Projetado para IA desde o primeiro dia. + actions: + - theme: brand + text: Começar → + link: /pt-br/tutorials/getting-started + - theme: alt + text: Ver no GitHub + link: https://github.com/hideyukiMORI/nene2-python + - theme: alt + text: Versão PHP + link: https://hideyukimori.github.io/NENE2/ + +features: + - icon: 🐍 + title: Python 3.12+ nativo + details: Sintaxe genérica do Python 3.12, dataclasses imutáveis e Pydantic v2. mypy --strict aplicado em todos os commits. + + - icon: ⚡ + title: FastAPI + async + details: ASGI nativo com AsyncUseCaseProtocol para I/O não bloqueante. asyncio.gather para chamadas paralelas. + + - icon: 🤖 + title: MCP integrado + details: UseCases expostos como ferramentas MCP via LocalMcpServer — sem configuração adicional. + + - icon: 🏛️ + title: Arquitetura Limpa + details: HTTP Handler → UseCase → RepositoryInterface → SQLAlchemy. Cada camada testável em isolamento. + + - icon: 🛡️ + title: Segurança em primeiro lugar + details: RFC 9457 Problem Details, autenticação Bearer + API Key, limitação de taxa, cabeçalhos de segurança. + + - icon: 📄 + title: OpenAPI gerado automaticamente + details: Swagger UI e ReDoc em /docs — sem configuração. Exporte um openapi.yaml estático com um comando. +--- diff --git a/docs/pt-br/tutorials/getting-started.md b/docs/pt-br/tutorials/getting-started.md new file mode 100644 index 0000000..b71f052 --- /dev/null +++ b/docs/pt-br/tutorials/getting-started.md @@ -0,0 +1,54 @@ +# Primeiros passos com nene2-python + +Este tutorial permite que você inicie uma API CRUD de Notes com nene2-python em 5 minutos. + +## Pré-requisitos + +- Python 3.12 ou superior +- [uv](https://docs.astral.sh/uv/) instalado +- Git + +## 1. Clonar o repositório + +```bash +git clone https://github.com/hideyukiMORI/nene2-python.git +cd nene2-python +``` + +## 2. Instalar dependências + +```bash +uv sync +``` + +## 3. Iniciar o servidor de desenvolvimento + +```bash +uv run uvicorn src.example.app:app --reload --port 8080 +``` + +Abra `http://localhost:8080/docs` no navegador para acessar o Swagger UI. + +## 4. Testar a API + +```bash +# Criar uma nota +curl -X POST http://localhost:8080/notes \ + -H "Content-Type: application/json" \ + -d '{"title": "Minha primeira nota", "body": "Criada com nene2-python"}' + +# Listar notas +curl http://localhost:8080/notes +``` + +## 5. Executar os testes + +```bash +uv run pytest +``` + +Mais de 135 testes devem passar com sucesso. + +## Próximos passos + +- [Referência de configuração](../reference/configuration.md) — Configurar banco de dados e autenticação via variáveis de ambiente diff --git a/docs/reference/api.md b/docs/reference/api.md index 3a25826..24f6555 100644 --- a/docs/reference/api.md +++ b/docs/reference/api.md @@ -1,9 +1,9 @@ -# REST API リファレンス +# REST API reference -nene2-python example アプリが提供するエンドポイントの一覧です。 +Endpoints provided by the nene2-python example application. -> OpenAPI スキーマ(機械可読)は `uv run export-openapi` で `docs/openapi.yaml` に生成できます。 -> 開発サーバー起動後は `http://localhost:8080/docs` で Swagger UI を参照できます。 +> Machine-readable schema: run `uv run export-openapi` to generate `docs/openapi.yaml`. +> Interactive docs: start the dev server and open `http://localhost:8080/docs`. --- @@ -11,125 +11,111 @@ nene2-python example アプリが提供するエンドポイントの一覧で ### `GET /notes` -ノート一覧を取得します。 +List notes with pagination. -**クエリパラメータ** +**Query parameters** -| パラメータ | 型 | デフォルト | 説明 | +| Parameter | Type | Default | Description | |---|---|---|---| -| `limit` | int | 20 | 最大取得件数(上限 100) | -| `offset` | int | 0 | スキップ件数 | +| `limit` | int | 20 | Maximum results (max 100) | +| `offset` | int | 0 | Number of results to skip | -**レスポンス** `200 OK` +**Response** `200 OK` ```json { - "items": [ - {"id": 1, "title": "ノートタイトル", "body": "本文"} - ], + "items": [{"id": 1, "title": "My note", "body": "Content"}], "limit": 20, "offset": 0, "total": 1 } ``` ---- - ### `POST /notes` -ノートを作成します。 +Create a note. -**リクエストボディ** +**Request body** ```json -{"title": "タイトル", "body": "本文"} +{"title": "Title", "body": "Body text"} ``` -**レスポンス** `201 Created` +**Response** `201 Created` ```json -{"id": 1, "title": "タイトル", "body": "本文"} +{"id": 1, "title": "Title", "body": "Body text"} ``` ---- - ### `GET /notes/{note_id}` -指定した ID のノートを取得します。 - -**レスポンス** `200 OK` / `404 Not Found` - ---- +Get a single note. Returns `404` if not found. ### `PUT /notes/{note_id}` -ノートを更新します。 +Update a note. -**リクエストボディ** +**Request body** ```json -{"title": "新タイトル", "body": "新本文"} +{"title": "New title", "body": "New body"} ``` -**レスポンス** `200 OK` / `404 Not Found` - ---- +**Response** `200 OK` / `404 Not Found` ### `DELETE /notes/{note_id}` -ノートを削除します。 - -**レスポンス** `204 No Content` / `404 Not Found` +Delete a note. Returns `204 No Content` / `404 Not Found`. --- ## Tags -`/tags` エンドポイントは Notes と同じ CRUD パターンです。 +`/tags` follows the same CRUD pattern as Notes. -| メソッド | パス | 説明 | +| Method | Path | Description | |---|---|---| -| `GET` | `/tags` | タグ一覧 | -| `POST` | `/tags` | タグ作成(`{"name": "..."}`) | -| `GET` | `/tags/{tag_id}` | タグ取得 | -| `PUT` | `/tags/{tag_id}` | タグ更新(`{"name": "..."}`) | -| `DELETE` | `/tags/{tag_id}` | タグ削除 | +| `GET` | `/tags` | List tags | +| `POST` | `/tags` | Create tag (`{"name": "..."}`) | +| `GET` | `/tags/{tag_id}` | Get tag | +| `PUT` | `/tags/{tag_id}` | Update tag (`{"name": "..."}`) | +| `DELETE` | `/tags/{tag_id}` | Delete tag | --- ## Comments -コメントはノートに紐づくネストリソースです。 +Comments are nested under a Note. -| メソッド | パス | 説明 | +| Method | Path | Description | |---|---|---| -| `GET` | `/notes/{note_id}/comments` | コメント一覧 | -| `POST` | `/notes/{note_id}/comments` | コメント作成(`{"body": "..."}`) | -| `GET` | `/notes/{note_id}/comments/{comment_id}` | コメント取得 | -| `PUT` | `/notes/{note_id}/comments/{comment_id}` | コメント更新 | -| `DELETE` | `/notes/{note_id}/comments/{comment_id}` | コメント削除 | +| `GET` | `/notes/{note_id}/comments` | List comments | +| `POST` | `/notes/{note_id}/comments` | Create comment (`{"body": "..."}`) | +| `GET` | `/notes/{note_id}/comments/{comment_id}` | Get comment | +| `PUT` | `/notes/{note_id}/comments/{comment_id}` | Update comment | +| `DELETE` | `/notes/{note_id}/comments/{comment_id}` | Delete comment | --- -## Health Check +## Health ### `GET /health` -アプリケーションの稼働状態を返します。 +Returns application health status. -**レスポンス** `200 OK` +**Response** `200 OK` ```json {"status": "ok", "db": "ok"} ``` -DB 接続失敗時は `db` フィールドが `"error"` になります。 +`db` is `"error"` when the database connection fails. --- -## エラーレスポンス +## Error responses -すべてのエラーは RFC 9457 Problem Details 形式で返します。 +All errors use [RFC 9457 Problem Details](https://www.rfc-editor.org/rfc/rfc9457) format. ```json { @@ -140,12 +126,12 @@ DB 接続失敗時は `db` フィールドが `"error"` になります。 } ``` -| ステータス | type | 原因 | +| Status | type | Cause | |---|---|---| -| 400 | `bad-request` | 不正なリクエスト | -| 401 | `unauthorized` | 認証失敗 | -| 404 | `not-found` | リソースが存在しない | -| 413 | `payload-too-large` | ペイロードサイズ超過 | -| 422 | `validation-failed` | バリデーションエラー | -| 429 | `too-many-requests` | レートリミット超過 | -| 500 | `internal-server-error` | サーバー内部エラー | +| 400 | `bad-request` | Malformed request | +| 401 | `unauthorized` | Authentication failure | +| 404 | `not-found` | Resource does not exist | +| 413 | `payload-too-large` | Body exceeds size limit | +| 422 | `validation-failed` | Input validation error | +| 429 | `too-many-requests` | Rate limit exceeded | +| 500 | `internal-server-error` | Unhandled server error | diff --git a/docs/reference/configuration.md b/docs/reference/configuration.md index 49a402e..187e86c 100644 --- a/docs/reference/configuration.md +++ b/docs/reference/configuration.md @@ -1,25 +1,25 @@ -# 設定リファレンス(環境変数) +# Configuration reference -設定は `pydantic-settings` で管理されており、環境変数または `.env` ファイルから読み込みます。 +All settings are managed by `AppSettings` (Pydantic Settings) and can be provided via environment variables or a `.env` file. -## 基本設定 +## Core -| 変数 | デフォルト | 説明 | +| Variable | Default | Description | |---|---|---| -| `APP_ENV` | `local` | 実行環境。`local` / `test` / `production` | -| `APP_DEBUG` | `false` | `true` の場合、500 エラーに例外メッセージを含める | -| `APP_NAME` | `nene2-python` | アプリケーション名 | +| `APP_ENV` | `local` | Runtime environment: `local` / `test` / `production` | +| `APP_DEBUG` | `false` | Include exception messages in 500 responses when `true` | +| `APP_NAME` | `nene2-python` | Application name | -## セキュリティ設定 +## Security -| 変数 | デフォルト | 説明 | +| Variable | Default | Description | |---|---|---| -| `SECURITY_HEADERS_ENABLED` | `true` | セキュリティヘッダー付与を有効化 | -| `MAX_BODY_SIZE` | `1048576` | リクエストボディの最大バイト数(デフォルト 1 MiB) | +| `SECURITY_HEADERS_ENABLED` | `true` | Add security headers to every response | +| `MAX_BODY_SIZE` | `1048576` | Maximum request body size in bytes (default 1 MiB) | -セキュリティヘッダーの内容: +Security headers added when enabled: -| ヘッダー | 値 | +| Header | Value | |---|---| | `X-Content-Type-Options` | `nosniff` | | `X-Frame-Options` | `DENY` | @@ -27,62 +27,61 @@ | `Content-Security-Policy` | `default-src 'self'` | | `Permissions-Policy` | `geolocation=(), microphone=()` | -## レートリミット +## Rate limiting -| 変数 | デフォルト | 説明 | +| Variable | Default | Description | |---|---|---| -| `THROTTLE_ENABLED` | `true` | レートリミットを有効化 | -| `THROTTLE_LIMIT` | `60` | ウィンドウ内の最大リクエスト数 | -| `THROTTLE_WINDOW` | `60` | ウィンドウの秒数 | +| `THROTTLE_ENABLED` | `true` | Enable rate limiting | +| `THROTTLE_LIMIT` | `60` | Maximum requests per window | +| `THROTTLE_WINDOW` | `60` | Window size in seconds | -固定ウィンドウ方式(IP アドレスをキーとする)。制限超過時は `429 Too Many Requests` + `Retry-After` ヘッダー。 +Uses a fixed-window algorithm keyed on client IP. Exceeding the limit returns `429 Too Many Requests` with a `Retry-After` header. -## CORS 設定 +## CORS -| 変数 | デフォルト | 説明 | +| Variable | Default | Description | |---|---|---| -| `CORS_ENABLED` | `false` | CORS ミドルウェアを有効化 | -| `CORS_ORIGINS` | `[]` | 許可オリジンのリスト(カンマ区切り) | -| `CORS_ALLOW_CREDENTIALS` | `false` | クレデンシャルを許可するか | -| `CORS_ALLOW_METHODS` | `GET,POST,PUT,DELETE,OPTIONS` | 許可メソッド | -| `CORS_ALLOW_HEADERS` | `*` | 許可ヘッダー | +| `CORS_ENABLED` | `false` | Enable CORS middleware | +| `CORS_ORIGINS` | `[]` | Allowed origins (comma-separated) | +| `CORS_ALLOW_CREDENTIALS` | `false` | Allow credentials | +| `CORS_ALLOW_METHODS` | `GET,POST,PUT,DELETE,OPTIONS` | Allowed methods | +| `CORS_ALLOW_HEADERS` | `*` | Allowed headers | -> `CORS_ORIGINS=*` は禁止です。許可オリジンを明示してください。 +> `CORS_ORIGINS=*` is **prohibited**. Always specify explicit origins. -## 認証設定 +## Authentication -| 変数 | デフォルト | 説明 | +| Variable | Default | Description | |---|---|---| -| `BEARER_TOKEN_ENABLED` | `false` | Bearer Token 認証を有効化 | -| `BEARER_TOKENS` | `[]` | 有効なトークンのリスト(カンマ区切り) | -| `API_KEY_ENABLED` | `false` | API Key 認証を有効化 | -| `API_KEYS` | `[]` | 有効な API キーのリスト(カンマ区切り) | +| `BEARER_TOKEN_ENABLED` | `false` | Enable Bearer Token auth | +| `BEARER_TOKENS` | `[]` | Valid tokens (comma-separated) | +| `API_KEY_ENABLED` | `false` | Enable API Key auth | +| `API_KEYS` | `[]` | Valid API keys (comma-separated) | -## データベース設定 +## Database -| 変数 | デフォルト | 説明 | +| Variable | Default | Description | |---|---|---| | `DB_ADAPTER` | `sqlite` | `sqlite` / `mysql` / `pgsql` | -| `DB_NAME` | `:memory:` | SQLite のファイルパスまたは DB 名 | -| `DB_HOST` | `localhost` | DB ホスト(SQLite では無視) | -| `DB_PORT` | `3306` | DB ポート(SQLite では無視) | -| `DB_USER` | `""` | DB ユーザー名(SQLite では無視) | -| `DB_PASSWORD` | `""` | DB パスワード — `SecretStr` 型(ログに出力されない) | +| `DB_NAME` | `:memory:` | SQLite file path or DB name | +| `DB_HOST` | `localhost` | Database host (ignored for SQLite) | +| `DB_PORT` | `3306` | Database port (ignored for SQLite) | +| `DB_USER` | `""` | Database user (ignored for SQLite) | +| `DB_PASSWORD` | `""` | Database password — stored as `SecretStr`, never logged | -### 接続 URL の例 +### Generated connection URLs -| アダプター | 生成される URL | +| Adapter | URL format | |---|---| | `sqlite` | `sqlite:///path/to/db.sqlite3` | | `mysql` | `mysql+pymysql://user:pass@host:3306/dbname` | | `pgsql` | `postgresql+psycopg2://user:pass@host:5432/dbname` | -## .env ファイル例 +## Example `.env` ```dotenv APP_ENV=production APP_DEBUG=false -APP_NAME=my-api THROTTLE_ENABLED=true THROTTLE_LIMIT=100 @@ -102,4 +101,4 @@ DB_USER=myuser DB_PASSWORD=supersecret ``` -> `.env` ファイルは `.gitignore` で除外してください。`.env.example` にキー一覧をコミットしてください。 +> Commit `.env.example` with empty values. Keep the real `.env` in `.gitignore`. diff --git a/docs/reference/framework-modules.md b/docs/reference/framework-modules.md index 9d9e065..1380b4a 100644 --- a/docs/reference/framework-modules.md +++ b/docs/reference/framework-modules.md @@ -1,12 +1,14 @@ -# フレームワークモジュールリファレンス +# Framework modules reference -`src/nene2/` パッケージが提供するコアモジュールの一覧です。 +Public API of the `nene2` package. + +--- ## nene2.http ### `PaginationQueryParser` -クエリパラメータ `limit` と `offset` を解析します。 +Parses `limit` and `offset` query parameters. ```python from nene2.http import PaginationQueryParser @@ -18,29 +20,23 @@ pagination = PaginationQueryParser.parse(request) ### `PaginationResponse` -ページネーションレスポンスの構造。 +Wraps a paginated result set. ```python from nene2.http import PaginationResponse -response = PaginationResponse(items=[...], limit=20, offset=0, total=42) -return JSONResponse(response.to_dict()) +body = PaginationResponse(items=[...], limit=20, offset=0, total=42).to_dict() # → {"items": [...], "limit": 20, "offset": 0, "total": 42} ``` ### `problem_details_response()` -RFC 9457 準拠のエラーレスポンスを生成します。 +Generates an RFC 9457 Problem Details response. ```python from nene2.http import problem_details_response -return problem_details_response( - problem_type="not-found", - title="Not Found", - status=404, - detail="Note with ID 42 was not found.", -) +return problem_details_response("not-found", "Not Found", 404, "Note 42 not found.") ``` --- @@ -49,7 +45,7 @@ return problem_details_response( ### `UseCaseProtocol[I, O]` -同期 UseCase の構造的型契約。 +Structural contract for synchronous UseCases. ```python from nene2.use_case import UseCaseProtocol @@ -62,7 +58,7 @@ assert isinstance(MyUseCase(), UseCaseProtocol) ### `AsyncUseCaseProtocol[I, O]` -非同期 UseCase の構造的型契約。 +Structural contract for async UseCases. ```python from nene2.use_case import AsyncUseCaseProtocol @@ -73,51 +69,43 @@ class MyAsyncUseCase: assert isinstance(MyAsyncUseCase(), AsyncUseCaseProtocol) ``` +> **Note**: `isinstance` checks attribute presence only. The async/sync distinction is enforced statically by `mypy --strict`. + --- ## nene2.config ### `AppSettings` -環境変数から設定を読み込む Pydantic Settings クラス。 -詳細は [設定リファレンス](configuration.md) を参照してください。 +Pydantic Settings class — reads from environment variables and `.env`. ```python from nene2.config import AppSettings -cfg = AppSettings() # 環境変数 / .env から読み込み -cfg_test = AppSettings(throttle_enabled=False) # テスト用オーバーライド +cfg = AppSettings() # from environment +cfg_test = AppSettings(throttle_enabled=False) # override for tests ``` +See [Configuration reference](configuration.md) for all fields. + --- ## nene2.middleware ### `ErrorHandlerMiddleware` -全例外をキャッチし RFC 9457 Problem Details に変換します。 -ドメイン例外ハンドラーは `DomainExceptionHandlerProtocol` を実装して登録します。 +Catches all unhandled exceptions and converts them to Problem Details responses. +Register domain exception handlers via `DomainExceptionHandlerProtocol`. -```python -from nene2.middleware import ErrorHandlerMiddleware -from nene2.middleware.domain_exception import DomainExceptionHandlerProtocol - -class MyExceptionHandler: - def handles(self, exc: Exception) -> bool: - return isinstance(exc, MyException) - def handle(self, exc: Exception) -> Response: - return problem_details_response("my-error", "My Error", 400) -``` +### Other middleware -### その他のミドルウェア - -| クラス | モジュール | 役割 | +| Class | Module | Role | |---|---|---| -| `SecurityHeadersMiddleware` | `nene2.middleware.security_headers` | セキュリティヘッダー付与 | -| `RequestIdMiddleware` | `nene2.middleware.request_id` | X-Request-ID 付与 | -| `RequestLoggingMiddleware` | `nene2.middleware.request_logging` | structlog リクエストロギング | -| `RequestSizeLimitMiddleware` | `nene2.middleware.request_size_limit` | ペイロードサイズ制限 | -| `ThrottleMiddleware` | `nene2.middleware.throttle` | 固定ウィンドウ レートリミット | +| `SecurityHeadersMiddleware` | `nene2.middleware.security_headers` | Add security response headers | +| `RequestIdMiddleware` | `nene2.middleware.request_id` | Generate / propagate `X-Request-ID` | +| `RequestLoggingMiddleware` | `nene2.middleware.request_logging` | Structured request / response logging | +| `RequestSizeLimitMiddleware` | `nene2.middleware.request_size_limit` | Reject oversized request bodies | +| `ThrottleMiddleware` | `nene2.middleware.throttle` | Fixed-window rate limiting per IP | --- @@ -125,7 +113,7 @@ class MyExceptionHandler: ### `LocalTokenVerifier` -`secrets.compare_digest` で静的トークンリストを検証します。 +Verifies tokens against a static list using `secrets.compare_digest`. ```python from nene2.auth import LocalTokenVerifier @@ -135,15 +123,43 @@ verifier.verify("token-a") # True verifier.verify("wrong") # False ``` -### `TokenVerifierProtocol` +### `TokenVerifierProtocol` / `TokenIssuerProtocol` + +Structural contracts for custom verifiers and issuers (e.g. JWT). + +### `TokenVerificationException` + +Raise this from a verifier to signal an invalid token. +`BearerTokenMiddleware` maps it to `401 Unauthorized`. + +--- + +## nene2.database + +### `SqlAlchemyQueryExecutor` + +Executes parameterised SQL via SQLAlchemy Core. + +```python +from nene2.database import SqlAlchemyQueryExecutor + +executor = SqlAlchemyQueryExecutor(engine) +rows = executor.fetch_all("SELECT * FROM notes WHERE id = :id", {"id": 1}) +executor.write("INSERT INTO notes (title, body) VALUES (:t, :b)", {"t": "t", "b": "b"}) +``` + +### `SqlAlchemyTransactionManager` -カスタム検証器の実装に使うプロトコル。 +Manages transactions. Prefer `transactional()` over manual `begin/commit/rollback`. ```python -from nene2.auth.interfaces import TokenVerifierProtocol +from nene2.database import SqlAlchemyTransactionManager + +mgr = SqlAlchemyTransactionManager(engine) -class JwtVerifier: - def verify(self, token: str) -> bool: ... +result = mgr.transactional( + lambda ex: ex.fetch_one("SELECT COUNT(*) AS cnt FROM notes") +) ``` --- @@ -152,33 +168,29 @@ class JwtVerifier: ### `LocalMcpServer` -MCP サーバーを構築するラッパー。 +Wraps FastMCP — registers UseCase functions as MCP tools. ```python from nene2.mcp import LocalMcpServer server = LocalMcpServer("my-server", instructions="...") -@server.tool("ノート一覧を取得する。") +@server.tool("List all notes.") def list_notes(limit: int = 20, offset: int = 0) -> list[dict]: ... -server.run(transport="stdio") # Claude Desktop 向け +server.run(transport="stdio") ``` ---- - -## nene2.database - -### `SqlAlchemyQueryExecutor` +### `HttpxMcpClient` -SQLAlchemy Core のラッパー。パラメータ化クエリを実行します。 +HTTP client for calling a nene2 API from MCP tool handlers. ```python -from nene2.database import SqlAlchemyQueryExecutor +from nene2.mcp import HttpxMcpClient -executor = SqlAlchemyQueryExecutor(engine) -rows = executor.query("SELECT * FROM notes WHERE id = :id", {"id": 1}) -executor.write("INSERT INTO notes (title, body) VALUES (:title, :body)", {...}) +client = HttpxMcpClient("bearer-token") +response = client.get("http://localhost:8080", "/notes") +response.is_successful() # True ``` --- @@ -187,13 +199,13 @@ executor.write("INSERT INTO notes (title, body) VALUES (:title, :body)", {...}) ### `setup_logging()` -structlog を初期化します。`app_env` に応じてレンダラーを切り替えます。 +Initialises structlog. Switches between ConsoleRenderer (local) and JSON (production). ```python from nene2.log import setup_logging -setup_logging(app_env="production") # JSON レンダラー -setup_logging(app_env="local") # Console レンダラー +setup_logging(app_env="production") # JSON renderer +setup_logging(app_env="local") # Console renderer ``` --- @@ -202,7 +214,7 @@ setup_logging(app_env="local") # Console レンダラー ### `ValidationException` / `ValidationError` -HTTP 入力検証失敗時に `422 Unprocessable Entity` を返す例外。 +Raise `ValidationException` at the HTTP boundary to return `422 Unprocessable Entity`. ```python from nene2.validation.exceptions import ValidationError, ValidationException diff --git a/docs/tutorials/first-domain.md b/docs/tutorials/first-domain.md index d64275c..bc85542 100644 --- a/docs/tutorials/first-domain.md +++ b/docs/tutorials/first-domain.md @@ -1,23 +1,23 @@ -# 新しいドメインを実装する +# Implement a new domain -このチュートリアルでは、`Tag` ドメインを例に nene2-python のクリーンアーキテクチャを体験します。 -各レイヤーを順番に実装することで、フレームワークの構造全体を理解できます。 +This tutorial walks you through adding the `Tag` domain from scratch. +By building each layer in order you will see how nene2-python's clean architecture fits together. -> **前提**: [はじめての nene2-python](getting-started.md) を完了していること +> **Prerequisite**: Complete [Getting started](getting-started.md) first. -## 実装するもの +## What we are building ``` -GET /tags — タグ一覧 -POST /tags — タグ作成 -GET /tags/{tag_id} — タグ取得 -PUT /tags/{tag_id} — タグ更新 -DELETE /tags/{tag_id} — タグ削除 +GET /tags — list tags +POST /tags — create a tag +GET /tags/{tag_id} — get a tag +PUT /tags/{tag_id} — update a tag +DELETE /tags/{tag_id} — delete a tag ``` -## ステップ 1: Entity を作る +## Step 1: Entity -`src/example/tag/entity.py` を作成します。 +Create `src/example/tag/entity.py`. ```python from dataclasses import dataclass @@ -28,11 +28,11 @@ class Tag: name: str ``` -`frozen=True` で不変オブジェクト、`slots=True` でメモリ効率を高めています。 +`frozen=True` makes the object immutable; `slots=True` reduces memory overhead. -## ステップ 2: Repository Interface を作る +## Step 2: Repository Interface -`src/example/tag/repository.py` に ABC を定義します。 +Define the contract in `src/example/tag/repository.py`. ```python from abc import ABC, abstractmethod @@ -58,9 +58,9 @@ class TagRepositoryInterface(ABC): def count(self) -> int: ... ``` -## ステップ 3: InMemory 実装を作る +## Step 3: InMemory implementation -テスト用の InMemory リポジトリを実装します。 +Provide an in-memory repository for tests — no database required. ```python class InMemoryTagRepository(TagRepositoryInterface): @@ -73,12 +73,12 @@ class InMemoryTagRepository(TagRepositoryInterface): self._store[self._next_id] = tag self._next_id += 1 return tag - # ... 省略 + # ... other methods ``` -## ステップ 4: UseCase を作る +## Step 4: UseCase -`src/example/tag/use_case.py` に UseCase を定義します。UseCase は HTTP・DB を知りません。 +`src/example/tag/use_case.py` contains business logic — no HTTP or database imports. ```python from dataclasses import dataclass @@ -98,9 +98,9 @@ class CreateTagUseCase: return self._repository.save(input_.name) ``` -## ステップ 5: Handler を作る +## Step 5: HTTP Handler -`src/example/tag/handler.py` に HTTP ルーターを定義します。**parse → use-case → response** の 3 ステップだけ。 +`src/example/tag/handler.py` — only three steps: **parse → use-case → response**. ```python from fastapi import APIRouter @@ -122,9 +122,9 @@ def make_tag_router(create_use_case: CreateTagUseCase, ...) -> APIRouter: return router ``` -## ステップ 6: app.py に組み込む +## Step 6: Wire into app.py -`src/example/app.py` の `create_app()` にルーターを追加します。 +Add the router in `src/example/app.py`. ```python app.include_router(make_tag_router( @@ -133,7 +133,7 @@ app.include_router(make_tag_router( )) ``` -## ステップ 7: テストを書く +## Step 7: Write tests ```python # tests/example/tag/test_tag_use_case.py @@ -143,12 +143,12 @@ def test_create_tag() -> None: assert tag.name == "python" ``` -## 完了 +## Done -実装した Tag ドメインは Comment ドメイン (`src/example/comment/`) と同じ構造です。 -実際の実装は `src/example/tag/` を参照してください。 +The Tag domain you just built matches the Comment domain at `src/example/comment/`. +See `src/example/tag/` for the full reference implementation. -## 次のステップ +## Next steps -- [新しいドメインを追加する](../how-to/add-new-domain.md) — チェックリスト形式の実践ガイド -- [アーキテクチャ概要](../explanation/architecture.md) — 各レイヤーの役割を深く理解する +- [Add a new domain](../how-to/add-new-domain.md) — a checklist for production-quality domain additions +- [Architecture overview](../explanation/architecture.md) — understand the role of each layer diff --git a/docs/tutorials/getting-started.md b/docs/tutorials/getting-started.md index ce903a2..9c19657 100644 --- a/docs/tutorials/getting-started.md +++ b/docs/tutorials/getting-started.md @@ -1,55 +1,55 @@ -# はじめての nene2-python +# Getting started with nene2-python -このチュートリアルでは、nene2-python を使って Note の CRUD API を 5 分で起動します。 +In this tutorial you will run a Note CRUD API in under 5 minutes. -## 前提条件 +## Prerequisites -- Python 3.12 以上 -- [uv](https://docs.astral.sh/uv/) がインストール済み +- Python 3.12 or later +- [uv](https://docs.astral.sh/uv/) installed - Git -## 1. リポジトリを clone する +## 1. Clone the repository ```bash git clone https://github.com/hideyukiMORI/nene2-python.git cd nene2-python ``` -## 2. 依存関係をインストールする +## 2. Install dependencies ```bash uv sync ``` -## 3. 開発サーバーを起動する +## 3. Start the development server ```bash uv run uvicorn src.example.app:app --reload --port 8080 ``` -起動後、ブラウザで `http://localhost:8080/docs` を開くと Swagger UI が表示されます。 +Open `http://localhost:8080/docs` in your browser — Swagger UI is ready. -## 4. API を試す +## 4. Try the API ```bash -# Note を作成する +# Create a note curl -X POST http://localhost:8080/notes \ -H "Content-Type: application/json" \ - -d '{"title": "はじめてのノート", "body": "nene2-python で作成しました"}' + -d '{"title": "My first note", "body": "Created with nene2-python"}' -# Note 一覧を取得する +# List notes curl http://localhost:8080/notes ``` -## 5. テストを実行する +## 5. Run the tests ```bash uv run pytest ``` -135 件以上のテストがすべて通ることを確認してください。 +All 165+ tests should pass. -## 次のステップ +## Next steps -- [新しいドメインを実装する](first-domain.md) — Tag ドメインの実装を通じてフレームワークの構造を理解する -- [設定リファレンス](../reference/configuration.md) — 環境変数で DB や認証を設定する +- [Implement a new domain](first-domain.md) — walk through the full layer stack using the Tag domain +- [Configuration reference](../reference/configuration.md) — configure a real database or enable auth diff --git a/docs/zh/index.md b/docs/zh/index.md new file mode 100644 index 0000000..0e884e8 --- /dev/null +++ b/docs/zh/index.md @@ -0,0 +1,43 @@ +--- +layout: home + +hero: + name: "NENE2" + text: "Python API 框架" + tagline: FastAPI · 整洁架构 · MCP · mypy --strict · 从第一天起就为 AI 而设计。 + actions: + - theme: brand + text: 开始使用 → + link: /zh/tutorials/getting-started + - theme: alt + text: 在 GitHub 上查看 + link: https://github.com/hideyukiMORI/nene2-python + - theme: alt + text: PHP 版本 + link: https://hideyukimori.github.io/NENE2/ + +features: + - icon: 🐍 + title: Python 3.12+ 原生 + details: 使用 Python 3.12 泛型语法、冻结 dataclass 和 Pydantic v2。每次提交都强制执行 mypy --strict。 + + - icon: ⚡ + title: FastAPI + 异步支持 + details: ASGI 原生,通过 AsyncUseCaseProtocol 支持非阻塞 I/O。使用 asyncio.gather 进行并发仓库调用。 + + - icon: 🤖 + title: 内置 MCP + details: 通过 LocalMcpServer 将 UseCase 作为 MCP 工具公开 — 无需额外配置。 + + - icon: 🏛️ + title: 整洁架构 + details: HTTP Handler → UseCase → RepositoryInterface → SQLAlchemy。每层都可使用 InMemory 仓库独立测试。 + + - icon: 🛡️ + title: 安全优先 + details: RFC 9457 Problem Details、Bearer + API Key 认证、速率限制、安全标头,开箱即用。 + + - icon: 📄 + title: 自动生成 OpenAPI + details: 在 /docs 提供 Swagger UI 和 ReDoc — 零配置。一条命令导出静态 openapi.yaml。 +--- diff --git a/docs/zh/tutorials/getting-started.md b/docs/zh/tutorials/getting-started.md new file mode 100644 index 0000000..95ff48c --- /dev/null +++ b/docs/zh/tutorials/getting-started.md @@ -0,0 +1,54 @@ +# nene2-python 入门 + +本教程将帮助您在 5 分钟内使用 nene2-python 启动一个 Notes CRUD API。 + +## 前提条件 + +- Python 3.12 或更高版本 +- 已安装 [uv](https://docs.astral.sh/uv/) +- Git + +## 1. 克隆仓库 + +```bash +git clone https://github.com/hideyukiMORI/nene2-python.git +cd nene2-python +``` + +## 2. 安装依赖 + +```bash +uv sync +``` + +## 3. 启动开发服务器 + +```bash +uv run uvicorn src.example.app:app --reload --port 8080 +``` + +在浏览器中打开 `http://localhost:8080/docs` 查看 Swagger UI。 + +## 4. 测试 API + +```bash +# 创建笔记 +curl -X POST http://localhost:8080/notes \ + -H "Content-Type: application/json" \ + -d '{"title": "我的第一条笔记", "body": "使用 nene2-python 创建"}' + +# 获取笔记列表 +curl http://localhost:8080/notes +``` + +## 5. 运行测试 + +```bash +uv run pytest +``` + +135 个以上的测试应全部通过。 + +## 下一步 + +- [配置参考](../reference/configuration.md) — 通过环境变量配置数据库和身份验证