From 46dfd27f111a93792b6a41e200d871b495318e03 Mon Sep 17 00:00:00 2001 From: hideyukiMORI Date: Tue, 19 May 2026 21:15:14 +0900 Subject: [PATCH] =?UTF-8?q?docs:=20Di=C3=A1taxis=20=E6=A7=8B=E9=80=A0?= =?UTF-8?q?=E3=81=A7=E3=83=89=E3=82=AD=E3=83=A5=E3=83=A1=E3=83=B3=E3=83=88?= =?UTF-8?q?=E3=82=92=E6=95=B4=E5=82=99=E3=81=99=E3=82=8B=20(#43)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 4.6 --- docs/explanation/architecture.md | 100 ++++++++++++ docs/explanation/design-philosophy.md | 68 +++++++++ docs/how-to/add-new-domain.md | 87 +++++++++++ docs/how-to/configure-auth.md | 84 ++++++++++ docs/how-to/run-tests.md | 98 ++++++++++++ docs/index.md | 44 ++++++ docs/reference/api.md | 151 ++++++++++++++++++ docs/reference/configuration.md | 105 +++++++++++++ docs/reference/framework-modules.md | 212 ++++++++++++++++++++++++++ docs/tutorials/first-domain.md | 154 +++++++++++++++++++ docs/tutorials/getting-started.md | 55 +++++++ 11 files changed, 1158 insertions(+) create mode 100644 docs/explanation/architecture.md create mode 100644 docs/explanation/design-philosophy.md create mode 100644 docs/how-to/add-new-domain.md create mode 100644 docs/how-to/configure-auth.md create mode 100644 docs/how-to/run-tests.md create mode 100644 docs/index.md create mode 100644 docs/reference/api.md create mode 100644 docs/reference/configuration.md create mode 100644 docs/reference/framework-modules.md create mode 100644 docs/tutorials/first-domain.md create mode 100644 docs/tutorials/getting-started.md diff --git a/docs/explanation/architecture.md b/docs/explanation/architecture.md new file mode 100644 index 0000000..b75e099 --- /dev/null +++ b/docs/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/explanation/design-philosophy.md b/docs/explanation/design-philosophy.md new file mode 100644 index 0000000..4bcb0f1 --- /dev/null +++ b/docs/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/how-to/add-new-domain.md b/docs/how-to/add-new-domain.md new file mode 100644 index 0000000..d866cd5 --- /dev/null +++ b/docs/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/how-to/configure-auth.md b/docs/how-to/configure-auth.md new file mode 100644 index 0000000..3f4ec6b --- /dev/null +++ b/docs/how-to/configure-auth.md @@ -0,0 +1,84 @@ +# 認証を設定する + +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 +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: + return False +``` + +`create_app()` の `verifier` 引数に渡すか、`BearerTokenMiddleware` を直接使います。 diff --git a/docs/how-to/run-tests.md b/docs/how-to/run-tests.md new file mode 100644 index 0000000..a079759 --- /dev/null +++ b/docs/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/index.md b/docs/index.md new file mode 100644 index 0000000..e3556c2 --- /dev/null +++ b/docs/index.md @@ -0,0 +1,44 @@ +# nene2-python ドキュメント + +NENE2 Python リファレンスフレームワークのドキュメントは [Diátaxis](https://diataxis.fr/) 構造で整理されています。 + +--- + +## Tutorials — 学習ガイド + +手を動かして学ぶ、段階的なチュートリアルです。 + +- [はじめての nene2-python](tutorials/getting-started.md) — 5 分で API サーバーを起動する +- [新しいドメインを実装する](tutorials/first-domain.md) — Tag ドメインをゼロから作る + +## How-to — 実践ガイド + +特定のタスクを達成するための具体的な手順書です。 + +- [新しいドメインを追加する](how-to/add-new-domain.md) +- [認証を設定する](how-to/configure-auth.md) +- [MCP サーバーをセットアップする](howto/mcp-setup.md) +- [テストを実行する](how-to/run-tests.md) + +## Explanation — 解説 + +設計の背景・思想・トレードオフを説明します。 + +- [アーキテクチャ概要](explanation/architecture.md) +- [設計思想と PHP NENE2 との対応](explanation/design-philosophy.md) +- [ADR 一覧](adr/) — 個別の設計決定記録 + +## Reference — リファレンス + +技術仕様の正確な記述です。 + +- [設定リファレンス(環境変数)](reference/configuration.md) +- [フレームワークモジュール](reference/framework-modules.md) +- [REST API リファレンス](reference/api.md) + +--- + +## プロジェクト管理 + +- [ロードマップ](roadmap.md) +- [フィールドトライアル記録](field-trials/) diff --git a/docs/reference/api.md b/docs/reference/api.md new file mode 100644 index 0000000..3a25826 --- /dev/null +++ b/docs/reference/api.md @@ -0,0 +1,151 @@ +# 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 のノートを取得します。 + +**レスポンス** `200 OK` / `404 Not Found` + +--- + +### `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/reference/configuration.md b/docs/reference/configuration.md new file mode 100644 index 0000000..49a402e --- /dev/null +++ b/docs/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/reference/framework-modules.md b/docs/reference/framework-modules.md new file mode 100644 index 0000000..9d9e065 --- /dev/null +++ b/docs/reference/framework-modules.md @@ -0,0 +1,212 @@ +# フレームワークモジュールリファレンス + +`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 + +response = PaginationResponse(items=[...], limit=20, offset=0, total=42) +return JSONResponse(response.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( + problem_type="not-found", + title="Not Found", + status=404, + detail="Note with ID 42 was 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) +``` + +--- + +## nene2.config + +### `AppSettings` + +環境変数から設定を読み込む Pydantic Settings クラス。 +詳細は [設定リファレンス](configuration.md) を参照してください。 + +```python +from nene2.config import AppSettings + +cfg = AppSettings() # 環境変数 / .env から読み込み +cfg_test = AppSettings(throttle_enabled=False) # テスト用オーバーライド +``` + +--- + +## nene2.middleware + +### `ErrorHandlerMiddleware` + +全例外をキャッチし RFC 9457 Problem Details に変換します。 +ドメイン例外ハンドラーは `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) +``` + +### その他のミドルウェア + +| クラス | モジュール | 役割 | +|---|---|---| +| `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` + +カスタム検証器の実装に使うプロトコル。 + +```python +from nene2.auth.interfaces import TokenVerifierProtocol + +class JwtVerifier: + def verify(self, token: str) -> bool: ... +``` + +--- + +## nene2.mcp + +### `LocalMcpServer` + +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") # Claude Desktop 向け +``` + +--- + +## nene2.database + +### `SqlAlchemyQueryExecutor` + +SQLAlchemy Core のラッパー。パラメータ化クエリを実行します。 + +```python +from nene2.database import SqlAlchemyQueryExecutor + +executor = SqlAlchemyQueryExecutor(engine) +rows = executor.query("SELECT * FROM notes WHERE id = :id", {"id": 1}) +executor.write("INSERT INTO notes (title, body) VALUES (:title, :body)", {...}) +``` + +--- + +## 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/tutorials/first-domain.md b/docs/tutorials/first-domain.md new file mode 100644 index 0000000..d64275c --- /dev/null +++ b/docs/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/tutorials/getting-started.md b/docs/tutorials/getting-started.md new file mode 100644 index 0000000..ce903a2 --- /dev/null +++ b/docs/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 や認証を設定する