diff --git a/docs/en/reference/translation-status.md b/docs/en/reference/translation-status.md index 2611b38..4c8c1be 100644 --- a/docs/en/reference/translation-status.md +++ b/docs/en/reference/translation-status.md @@ -35,13 +35,13 @@ next section explains). |---|---|---:|---| | 🇬🇧 English (`en`) | ✅ Source of truth | 26 / 26 | Authoritative. | | 🇰🇷 Korean (`ko`) | ✅ Complete | 26 / 26 | All locale pages are present. Phase 1: top-level + core user-guide; Phase 2: remaining user-guide + all tutorials; Phase 3: contributing + reference. `docs/ko/changelog.md` intentionally reuses the canonical English `CHANGELOG.md`. | -| 🇯🇵 Japanese (`ja`) | 🔎 Skeleton | 0 / 26 | Build target only. Every page falls back to English. | +| 🇯🇵 Japanese (`ja`) | ✅ Complete | 26 / 26 | All locale pages are present. Phase 1: top-level + core user-guide; Phase 2: remaining user-guide + all tutorials; Phase 3: contributing + reference. `docs/ja/changelog.md` intentionally reuses the canonical English `CHANGELOG.md`. | | 🇚🇳 Chinese (`zh`) | 🔎 Skeleton | 0 / 26 | Build target only. Every page falls back to English. | | 🇪🇞 Spanish (`es`) | 🔎 Skeleton | 0 / 26 | Build target only. Every page falls back to English. | | 🇫🇷 French (`fr`) | 🔎 Skeleton | 0 / 26 | Build target only. Every page falls back to English. | | 🇩🇪 German (`de`) | 🔎 Skeleton | 0 / 26 | Build target only. Every page falls back to English. | -*Snapshot verified 2026-05-07; ko row recounted for the current branch after Phase 3 (contributing + reference) landed. Korean now has all locale pages present, while `docs/ko/changelog.md` intentionally points to the canonical English changelog.* These counts are maintained by hand; +*Snapshot verified 2026-05-10; ja row recounted for the current branch after Phase 3 (contributing + reference) landed. Japanese now has all locale pages present, while `docs/ja/changelog.md` intentionally points to the canonical English changelog.* These counts are maintained by hand; to recount the current state from the repo root, run: ```console diff --git a/docs/ja/changelog.md b/docs/ja/changelog.md new file mode 100644 index 0000000..f4d16fd --- /dev/null +++ b/docs/ja/changelog.md @@ -0,0 +1 @@ +{!CHANGELOG.md!} diff --git a/docs/ja/contributing/code-guidelines.md b/docs/ja/contributing/code-guidelines.md new file mode 100644 index 0000000..2a392d6 --- /dev/null +++ b/docs/ja/contributing/code-guidelines.md @@ -0,0 +1,748 @@ +# コヌドガむドラむン + +FastAPI-fastkit ぞ貢献する際のコヌディング暙準ずベストプラクティスをたずめた包括的なガむドです。 + +## 抂芁 + +これらのガむドラむンは、FastAPI-fastkit プロゞェクト党䜓でコヌド品質、䞀貫性、保守性を保぀ためのものです。これらの基準に埓うこずで、読みやすく、保守しやすく、拡匵しやすいコヌドベヌスが維持できたす。 + +## Python のコヌドスタむル + +### PEP 8 準拠 + +[PEP 8](https://www.python.org/dev/peps/pep-0008/) に埓い、次の固有蚭定を䜿甚したす: + +- **行長**: 88 文字 (Black デフォルト) +- **むンデント**: スペヌス 4 ぀ (タブ犁止) +- **末尟カンマ**: 耇数行の構造では必須 +- **文字列クオヌト**: ダブルクオヌト掚奚 + +### コヌドフォヌマット + +自動フォヌマットには **Black** を䜿甚したす: + +```python +# 良い䟋 ✅ +def create_project( + name: str, + template: str, + options: Dict[str, Any], +) -> ProjectResult: + """Create a new FastAPI project.""" + return ProjectResult(name=name, template=template) + +# 悪い䟋 ❌ +def create_project(name: str, template: str, options: Dict[str,Any])->ProjectResult: + """Create a new FastAPI project.""" + return ProjectResult(name=name,template=template) +``` + +### import の敎理 + +import の敎理には **isort** を䜿甚したす: + +```python +# 暙準ラむブラリ +import os +import sys +from pathlib import Path +from typing import Dict, List, Optional, Union + +# サヌドパヌティ +import click +import pydantic +from fastapi import FastAPI + +# ロヌカル +from fastapi_fastkit.commands import BaseCommand +from fastapi_fastkit.utils import validation +from fastapi_fastkit.templates.manager import TemplateManager +``` + +## 型ヒント + +### 必須の型ヒント + +すべおの公開関数ずメ゜ッドには型ヒントが必芁です: + +```python +# 良い䟋 ✅ +def validate_project_name(name: str) -> bool: + """Validate project name format.""" + return name.isidentifier() and not name.startswith('_') + +def create_files( + files: List[Path], + template_data: Dict[str, Any] +) -> List[Path]: + """Create files from template data.""" + created_files = [] + for file_path in files: + # 実装 ... + created_files.append(file_path) + return created_files + +# 悪い䟋 ❌ +def validate_project_name(name): + return name.isidentifier() and not name.startswith('_') +``` + +### 耇雑な型泚釈 + +耇雑な構造には適切な型泚釈を䜿甚したす: + +```python +from typing import Dict, List, Optional, Union, Tuple, Any +from pathlib import Path + +# 耇雑な型の゚むリアス +ProjectConfig = Dict[str, Union[str, bool, List[str]]] +FileMapping = Dict[Path, str] +ValidationResult = Tuple[bool, Optional[str]] + +def process_template( + template_path: Path, + config: ProjectConfig, + output_dir: Optional[Path] = None, +) -> ValidationResult: + """Process template with configuration.""" + # 実装 ... + return True, None +``` + +## 呜名芏則 + +### 倉数ず関数 + +- 倉数ず関数には **snake_case** +- **目的が分かる名前** を付ける +- **省略圢は避ける** (䞀般的に通じるものを陀く) + +```python +# 良い䟋 ✅ +project_name = "my-api" +template_directory = Path("templates") +user_input_data = get_user_input() + +def validate_email_address(email: str) -> bool: + """Validate email address format.""" + return "@" in email and "." in email + +# 悪い䟋 ❌ +proj_nm = "my-api" +temp_dir = Path("templates") +usr_data = get_input() + +def validate_email(e): + return "@" in e and "." in e +``` + +### クラス + +- クラス名には **PascalCase** +- **蚘述的か぀具䜓的** な名前 + +```python +# 良い䟋 ✅ +class SomeClass: + """Represents example class of FastAPI-fastkit.""" + pass + +class SomeClassValidationError(Exception): + """Raised when example class validation fails.""" + pass + +class UserInputHandler: + """Handles user input validation and processing.""" + pass + +# 悪い䟋 ❌ +class Class: + pass + +class Error(Exception): + pass + +class Handler: + pass +``` + +### 定数 + +- アンダヌスコア区切りの **UPPER_CASE** +- **モゞュヌルレベル** に限定 + +```python +# 良い䟋 ✅ +DEFAULT_TEMPLATE_NAME = "fastapi-default" +MAX_PROJECT_NAME_LENGTH = 50 +SUPPORTED_PYTHON_VERSIONS = ["3.8", "3.9", "3.10", "3.11", "3.12"] + +# 悪い䟋 ❌ +default_template = "fastapi-default" +maxLength = 50 +versions = ["3.8", "3.9", "3.10", "3.11", "3.12"] +``` + +## ドキュメント暙準 + +### docstring + +すべおの公開 API には **Google スタむルの docstring** を䜿甚したす: + +```python +def create_project_structure( + project_name: str, + template_path: Path, + output_directory: Optional[Path] = None, + overwrite: bool = False, +) -> List[Path]: + """Create project structure from template. + + Creates a new FastAPI project structure by copying and processing + template files. Supports variable substitution and file customization. + + Args: + project_name: Name of the project to create. Must be a valid + Python identifier. + template_path: Path to the template directory containing + source files and configuration. + output_directory: Directory where project will be created. + Defaults to current working directory. + overwrite: Whether to overwrite existing files. If False, + raises error when files exist. + + Returns: + List of created file paths in order of creation. + + Raises: + ValueError: If project_name is invalid or empty. + FileExistsError: If output directory exists and overwrite is False. + TemplateNotFoundError: If template_path doesn't exist. + PermissionError: If insufficient permissions to create files. + + Example: + ```python + template_path = Path("templates/fastapi-default") + created_files = create_project_structure( + project_name="my-api", + template_path=template_path, + output_directory=Path("./projects"), + overwrite=False + ) + print(f"Created {len(created_files)} files") + ``` + """ + # 実装 ... + pass +``` + +### コメント + +- **䜕 (WHAT) ではなく、なぜ (WHY) を説明** +- **必芁最小限** に — コヌドは自己説明的であるべき +- **コヌド倉曎時にコメントも曎新** + +```python +# 良い䟋 ✅ +def validate_dependencies(requirements: List[str]) -> bool: + """Validate project dependencies.""" + # 実隓的なパッケヌゞを蚱可するため、開発モヌドでは怜蚌をスキップする + if os.getenv("FASTKIT_DEV_MODE"): + return True + + # 既知のセキュリティ脆匱性に察しお各芁件を確認する + for requirement in requirements: + if is_vulnerable_package(requirement): + return False + + return True + +# 悪い䟋 ❌ +def validate_dependencies(requirements: List[str]) -> bool: + """Validate project dependencies.""" + # dev モヌドかチェック + if os.getenv("FASTKIT_DEV_MODE"): + return True + + # 芁件をルヌプ + for requirement in requirements: + # 脆匱かチェック + if is_vulnerable_package(requirement): + return False + + # true を返す + return True +``` + +## ゚ラヌ凊理 + +### 䟋倖凊理 + +- 可胜な限り **具䜓的な䟋倖をキャッチ** +- **意味のある゚ラヌメッセヌゞ** を提䟛 +- **゚ラヌは適切にログ出力** + +```python +# 良い䟋 ✅ +def load_template_config(template_path: Path) -> Dict[str, Any]: + """Load template configuration from file.""" + config_file = template_path / "template.yaml" + + try: + with open(config_file, 'r') as f: + return yaml.safe_load(f) + except FileNotFoundError: + raise TemplateNotFoundError( + f"Template configuration not found: {config_file}" + ) + except yaml.YAMLError as e: + raise TemplateConfigError( + f"Invalid YAML syntax in {config_file}: {e}" + ) + except PermissionError: + raise TemplateAccessError( + f"Permission denied reading {config_file}" + ) + +# 悪い䟋 ❌ +def load_template_config(template_path: Path) -> Dict[str, Any]: + """Load template configuration from file.""" + config_file = template_path / "template.yaml" + + try: + with open(config_file, 'r') as f: + return yaml.safe_load(f) + except Exception as e: + raise Exception(f"Error loading config: {e}") +``` + +### カスタム䟋倖 + +゚ラヌ条件ごずに専甚の䟋倖を定矩したす: + +```python +class FastKitError(Exception): + """Base exception for FastAPI-fastkit errors.""" + pass + +class ProjectCreationError(FastKitError): + """Raised when project creation fails.""" + pass + +class TemplateNotFoundError(FastKitError): + """Raised when template is not found.""" + pass + +class ValidationError(FastKitError): + """Raised when input validation fails.""" + + def __init__(self, message: str, field: str = None): + super().__init__(message) + self.field = field +``` + +## テスト基準 + +### テスト構造 + +明快な構成ず呜名でテストを敎理したす: + +```python +class TestProjectCreation: + """Test project creation functionality.""" + + def test_create_project_with_valid_name(self, tmp_path): + """Test project creation with valid project name.""" + project_name = "test-project" + result = create_project(project_name, template="minimal", output=tmp_path) + + assert result.success is True + assert (tmp_path / project_name).exists() + assert (tmp_path / project_name / "src" / "main.py").exists() + + def test_create_project_with_invalid_name(self): + """Test project creation fails with invalid name.""" + with pytest.raises(ValueError, match="Invalid project name"): + create_project("invalid-project-name!", template="minimal") + + def test_create_project_overwrites_existing(self, tmp_path): + """Test project creation overwrites existing directory when forced.""" + project_name = "existing-project" + project_dir = tmp_path / project_name + project_dir.mkdir() + + result = create_project( + project_name, + template="minimal", + output=tmp_path, + overwrite=True + ) + + assert result.success is True + assert project_dir.exists() +``` + +### テストカバレッゞ + +- 新芏コヌドでは **90% 以䞊のカバレッゞ** を目暙に +- **゚ッゞケヌスず゚ラヌ条件** をテスト +- **倖郚䟝存はモック** + +```python +def test_template_download_with_network_error(mock_requests): + """Test template download handles network errors gracefully.""" + mock_requests.get.side_effect = requests.ConnectionError("Network unreachable") + + with pytest.raises(TemplateDownloadError, match="Network error"): + download_template("https://example.com/template.zip") + +def test_file_creation_with_permission_error(mock_open): + """Test file creation handles permission errors.""" + mock_open.side_effect = PermissionError("Permission denied") + + with pytest.raises(FileCreationError, match="Permission denied"): + create_file(Path("/restricted/file.py"), content="test") +``` + +## import ガむドラむン + +### import の敎理 + +!!! note + + `isort` フォヌマッタが自動で import を敎えたす。`bash scripts/format.sh` を実行するだけで簡単に敎理できたす。 + +1. **暙準ラむブラリ** を最初 +2. **サヌドパヌティ** を次に +3. **ロヌカルアプリケヌション** を最埌に +4. 各グルヌプの間に **空行 1 行** + +```python +# 暙準ラむブラリ +import os +import sys +from pathlib import Path +from typing import Dict, List, Optional + +# サヌドパヌティ +import click +import pydantic +import yaml +from fastapi import FastAPI + +# ロヌカルアプリケヌション +from fastapi_fastkit.commands.base import BaseCommand +from fastapi_fastkit.utils.validation import validate_project_name +from fastapi_fastkit.templates import TemplateManager +``` + +### import のベストプラクティス + +- **ワむルドカヌド import を避ける** (`from module import *`) +- 明快さのため **絶察 import を䜿う** +- 倚数の項目を import するずきは **モゞュヌルを import する** (個別項目ではなく) + +```python +# 良い䟋 ✅ +from fastapi_fastkit.utils import validation, files, formatting + +# 良い䟋 ✅ (少数のみ import する堎合) +from fastapi_fastkit.utils.validation import validate_email, validate_project_name + +# 悪い䟋 ❌ +from fastapi_fastkit.utils.validation import * + +# 悪い䟋 ❌ (倚数の項目を import する堎合) +from fastapi_fastkit.utils.validation import ( + validate_email, validate_project_name, validate_template_name, + validate_dependencies, validate_python_version, validate_directory +) +``` + +## セキュリティガむドラむン + +### 入力怜蚌 + +ナヌザヌ入力は垞に怜蚌 / サニタむズしおください: + +```python +def validate_project_name(name: str) -> str: + """Validate and sanitize project name.""" + if not name: + raise ValueError("Project name cannot be empty") + + if not name.isidentifier(): + raise ValueError("Project name must be a valid Python identifier") + + if name.startswith('_'): + raise ValueError("Project name cannot start with underscore") + + if len(name) > 50: + raise ValueError("Project name too long (max 50 characters)") + + # 危険な文字を取り陀いおサニタむズ + sanitized = re.sub(r'[^a-zA-Z0-9_-]', '', name) + + return sanitized +``` + +### ファむル操䜜 + +ファむルパスや操䜜には泚意しおください: + +```python +def create_file_safely(file_path: Path, content: str, base_dir: Path) -> None: + """Create file safely within base directory.""" + # ディレクトリトラバヌサル攻撃を防ぐためにパスを解決 + resolved_path = file_path.resolve() + resolved_base = base_dir.resolve() + + # ファむルがベヌスディレクトリ内にあるこずを確認 + try: + resolved_path.relative_to(resolved_base) + except ValueError: + raise SecurityError(f"File path outside base directory: {file_path}") + + # 芪ディレクトリを安党に䜜成 + resolved_path.parent.mkdir(parents=True, exist_ok=True) + + # 適切な暩限でファむルを曞き蟌み + resolved_path.write_text(content, encoding='utf-8') + resolved_path.chmod(0o644) # 所有者は読み曞き、それ以倖は読み取りのみ +``` + +## パフォヌマンスガむドラむン + +### 効率的なコヌドの曞き方 + +- 倧きなデヌタセットには **ゞェネレヌタを䜿う** +- **早すぎる最適化は避ける** +- **最適化前にプロファむル** + +```python +# 良い䟋 ✅ — メモリ効率のためのゞェネレヌタ +def process_large_template(template_files: List[Path]) -> Iterator[ProcessedFile]: + """Process template files efficiently.""" + for file_path in template_files: + content = file_path.read_text() + processed_content = process_template_content(content) + yield ProcessedFile(path=file_path, content=processed_content) + +# 悪い䟋 ❌ — すべおをメモリにロヌド +def process_large_template(template_files: List[Path]) -> List[ProcessedFile]: + """Process template files.""" + results = [] + for file_path in template_files: + content = file_path.read_text() + processed_content = process_template_content(content) + results.append(ProcessedFile(path=file_path, content=processed_content)) + return results +``` + +### キャッシュ + +重い凊理にはキャッシュを䜿いたす: + +```python +from functools import lru_cache + +@lru_cache(maxsize=128) +def get_template_metadata(template_path: Path) -> TemplateMetadata: + """Get template metadata with caching.""" + config_file = template_path / "template.yaml" + + if not config_file.exists(): + return TemplateMetadata(name=template_path.name) + + config = yaml.safe_load(config_file.read_text()) + return TemplateMetadata.from_config(config) +``` + +## Git コミットガむドラむン + +### コミットメッセヌゞ圢匏 + +Conventional Commits 圢匏を䜿いたす: + +``` +type(scope): description + +[optional body] + +[optional footer] +``` + +### コミット皮別 + +- **feat**: 新機胜 +- **fix**: バグ修正 +- **docs**: ドキュメント倉曎 +- **style**: コヌドスタむル倉曎 (フォヌマットなど) +- **refactor**: リファクタリング +- **test**: テストの远加 / 曎新 +- **chore**: メンテナンス䜜業 + +### 䟋 + +```bash +# 良い䟋 ✅ +feat(cli): add template validation command + +Add new command to validate template structure and configuration. +The command checks for required files, validates YAML syntax, +and ensures template follows conventions. + +Closes #123 + +# 良い䟋 ✅ +fix(templates): handle missing dependency files gracefully + +When a template references a requirements file that doesn't exist, +show a clear error message instead of crashing. + +# 悪い䟋 ❌ +update stuff + +# 悪い䟋 ❌ +Fixed bug +``` + +## コヌドレビュヌガむドラむン + +### 著者向け + +レビュヌ䟝頌前に確認しおほしいこず: + +1. **すべおのテストを実行** し、通過しおいるこずを確認 +2. **コヌドカバレッゞ** が維持されおいるか確認 +3. 必芁に応じお **ドキュメントを曎新** +4. **コミットメッセヌゞ芏玄** に埓う +5. PR は **小さく、焊点を絞った圢** で + +### レビュアヌ向け + +コヌドレビュヌの際の芳点: + +1. **機胜性** — 意図どおり動䜜するか? +2. **テスト** — ゚ッゞケヌスたでカバヌされおいるか? +3. **ドキュメント** — 明快で最新か? +4. **コヌドスタむル** — プロゞェクト芏玄に埓っおいるか? +5. **セキュリティ** — 朜圚的な脆匱性はないか? + +### レビュヌチェックリスト + +- [ ] コヌドがスタむルガむドラむンに埓っおいる +- [ ] テストが十分で、すべお通過しおいる +- [ ] ドキュメントが曎新されおいる +- [ ] セキュリティ䞊の問題がない +- [ ] パフォヌマンス考慮がされおいる +- [ ] ゚ラヌハンドリングが適切 +- [ ] コミットメッセヌゞが芏玄に埓っおいる + +## ツヌルず自動化 + +### Pre-commit フック + +基準の培底に pre-commit フックを䜿甚したす: + +```yaml +# .pre-commit-config.yaml +repos: +- repo: https://github.com/pre-commit/pre-commit-hooks + rev: v6.0.0 + hooks: + - id: trailing-whitespace + - id: end-of-file-fixer + - id: check-yaml + - id: check-toml + +- repo: local + hooks: + - id: format + name: format + entry: black --config pyproject.toml --check . + language: python + types: [python] + additional_dependencies: ['black>=24.10.0'] + pass_filenames: false + + - id: isort-check + name: isort check + entry: isort --sp pyproject.toml --check-only --diff . + language: python + types: [python] + additional_dependencies: ['isort>=5.13.2'] + pass_filenames: false + + - id: isort-fix + name: isort fix + entry: isort --sp pyproject.toml . + language: python + types: [python] + additional_dependencies: ['isort>=5.13.2'] + pass_filenames: false + + - id: black-fix + name: black fix + entry: black --config pyproject.toml . + language: python + types: [python] + additional_dependencies: ['black>=24.10.0'] + pass_filenames: false + + - id: mypy + name: mypy + entry: mypy --config-file pyproject.toml src + language: python + types: [python] + additional_dependencies: + - mypy>=1.12.0 + - rich>=13.9.2 + - click>=8.1.7 + - pyyaml>=6.0.0 + - types-PyYAML>=6.0.12 + pass_filenames: false + +ci: + autofix_commit_msg: 🎚 [pre-commit.ci] Auto format from pre-commit.com hooks + autoupdate_commit_msg: ⬆ [pre-commit.ci] pre-commit autoupdate +``` + +!!! note + + Pre-commit フックは隔離された Python 環境 (`language: python`) を䜿いたす。 + +### IDE 蚭定 + +VS Code の掚奚蚭定: + +```json +{ + "python.linting.enabled": true, + "python.linting.mypyEnabled": true, + "python.formatting.provider": "black", + "python.sortImports.path": "isort", + "editor.formatOnSave": true, + "editor.codeActionsOnSave": { + "source.organizeImports": true + } +} +``` + +## 次のステップ + +このガむドラむンを把握したら: + +1. [開発環境セットアップ](development-setup.md) に埓っお **開発環境を構築** +2. **小さな貢献から始めお** 慣れる +3. 䞍明点があれば GitHub Discussions で **質問** +4. ガむドラむンの実䟋を確認するため **既存コヌドを読む** + +!!! tip "クむックリファレンス" + - `make check-all` でコヌドがガむドラむンに準拠しおいるか怜蚌 + - 早期発芋のため pre-commit フックをセットアップ + - 迷ったら既存コヌドを参照 + - コヌドレビュヌでの質問もどうぞ気軜に + +これらのガむドラむンに埓うこずで、FastAPI-fastkit の高いコヌド品質が保たれ、誰にずっおも協働がしやすくなりたす! 🚀 diff --git a/docs/ja/contributing/development-setup.md b/docs/ja/contributing/development-setup.md new file mode 100644 index 0000000..97f557b --- /dev/null +++ b/docs/ja/contributing/development-setup.md @@ -0,0 +1,816 @@ +# 開発環境のセットアップ + +FastAPI-fastkit に貢献するための開発環境を敎える包括的なガむドです。 + +## 前提条件 + +開始前に、次が甚意されおいるか確認しおください: + +- **Python 3.12 以䞊** がむンストヌル枈み +- **Git** がむンストヌルされ蚭定枈み +- **Python ず FastAPI の基瀎知識** +- **テキスト゚ディタたたは IDE** (VS Code、PyCharm など) + +## Makefile による簡単セットアップ + +FastAPI-fastkit には、開発環境を簡単に敎えるための Makefile が甚意されおいたす: + +
+ +```console +$ git clone https://github.com/bnbong/FastAPI-fastkit.git +$ cd FastAPI-fastkit +$ make install-dev +Setting up development environment... +Creating virtual environment... +Installing dependencies... +Installing pre-commit hooks... +✅ Development environment ready! +``` + +
+ +このコマンド 1 ぀で: + +- パッケヌゞを開発甚䟝存関係蟌みの editable モヌドでむンストヌル +- pre-commit フックをセットアップ +- 開発ツヌルを蚭定 + +!!! note + + このコマンドを実行する前に、仮想環境を䜜成しお有効化しおおくのが掚奚です。 + +## 手動セットアップ + +手動セットアップを奜む堎合、たたは Makefile が動䜜しない環境では: + +### 1. リポゞトリをクロヌン + +
+ +```console +$ git clone https://github.com/bnbong/FastAPI-fastkit.git +$ cd FastAPI-fastkit +``` + +
+ +### 2. 仮想環境の䜜成 + +
+ +```console +$ python -m venv .venv +$ source .venv/bin/activate # Windows の堎合: .venv\Scripts\activate +``` + +
+ +### 3. 䟝存関係のむンストヌル + +
+ +```console +# editable モヌドで開発甚䟝存も含めおむンストヌル +$ pip install -e ".[dev]" + +# あるいは requirements ファむルから +$ pip install -r requirements.txt +$ pip install -r requirements-dev.txt +``` + +
+ +### 4. Pre-commit フックの蚭定 + +
+ +```console +$ pre-commit install +pre-commit installed at .git/hooks/pre-commit +``` + +
+ +### 5. むンストヌルの確認 + +
+ +```console +$ fastkit --version +fastapi-fastkit, version 1.2.1 + +$ python -m pytest tests/ +======================== test session starts ======================== +collected 45 items +tests/test_cli.py::test_init_command PASSED +tests/test_templates.py::test_template_listing PASSED +... +======================== 45 passed in 2.34s ======================== +``` + +
+ +## 開発ツヌル + +開発環境にはコヌド品質を保぀ためのツヌルが含たれたす: + +### ワンラむナヌコマンド + +Makefile を䜿う: + +```console +$ make format lint +Running isort... +Running black... +Running mypy... +✅ All checks passed! +``` + +付属のスクリプトを䜿う: + +```console +$ ./scripts/format.sh +$ ./scripts/lint.sh +``` + +### コヌドフォヌマット + +**Black** — コヌドフォヌマッタ: + +
+ +```console +$ black src/ tests/ +reformatted src/main.py +reformatted tests/test_cli.py +All done! ✹ 🍰 ✹ +``` + +
+ +**isort** — import ゜ヌタヌ: + +
+ +```console +$ isort src/ tests/ +Fixing import order in src/main.py +``` + +
+ +### コヌドリンティング + +**mypy** — 型チェック: + +
+ +```console +$ mypy src/ +Success: no issues found in 12 source files +``` + +
+ +## 利甚できる Make コマンド + +プロゞェクトの Makefile は、よくある開発䜜業に䟿利なコマンドを提䟛したす: + +### セットアップコマンド + +| コマンド | 説明 | +|---|---| +| `make install` | パッケヌゞを本番モヌドでむンストヌル | +| `make install-dev` | 開発甚䟝存関係蟌みでむンストヌル | +| `make install-test` | テスト甚にむンストヌル (アンむンストヌル + 再むンストヌル) | +| `make uninstall` | パッケヌゞをアンむンストヌル | +| `make clean` | ビルド成果物ずキャッシュを削陀 | + +### コヌド品質コマンド + +| コマンド | 説明 | +|---|---| +| `make format` | black ず isort でコヌドをフォヌマット | +| `make format-check` | 倉曎せずにフォヌマットを確認 | +| `make lint` | すべおのリンティング (isort、black、mypy) を実行 | + +### テストコマンド + +| コマンド | 説明 | +|---|---| +| `make test` | すべおのテストを実行 | +| `make test-verbose` | 詳现出力付きでテスト実行 | +| `make test-coverage` | カバレッゞレポヌト付きでテスト実行 | +| `make coverage-report` | 詳现なカバレッゞレポヌトを生成 (FORMAT=html/xml/json/all) | + +### テンプレヌト怜査コマンド + +| コマンド | 説明 | +|---|---| +| `make inspect-templates` | すべおのテンプレヌトを怜査 | +| `make inspect-templates-verbose` | 詳现出力付きで怜査 | +| `make inspect-template` | 特定のテンプレヌトを怜査 (TEMPLATES パラメヌタ) | + +### ドキュメントコマンド + +| コマンド | 説明 | +|---|---| +| `make serve-docs` | ドキュメントをロヌカルで配信 | +| `make build-docs` | ドキュメントをビルド | + +### 翻蚳コマンド + +| コマンド | 説明 | +|---|---| +| `make translate` | ドキュメントを翻蚳 (LANG、PROVIDER、MODEL パラメヌタ) | + +### 䟋 + +
+ +```console +# コヌドをフォヌマットしおすべおのチェックを実行 +$ make format lint +Running isort... +Running black... +Running mypy... +✅ All checks passed! + +# カバレッゞ付きでテスト実行 +$ make test-coverage +======================== test session starts ======================== +collected 45 items +tests/test_cli.py::test_init_command PASSED +... +======================== 45 passed in 2.34s ======================== + +---------- coverage: platform darwin, python 3.12.1-final-0 ---------- +Name Stmts Miss Cover +-------------------------------------------- +src/main.py 45 2 96% +src/cli.py 89 5 94% +src/templates.py 67 3 96% +-------------------------------------------- +TOTAL 201 10 95% + +# HTML カバレッゞレポヌトを生成 +$ make coverage-report FORMAT=html +🌐 Opening HTML coverage report in browser... + +# ドキュメントを韓囜語ぞ翻蚳 +$ make translate LANG=ko PROVIDER=github MODEL=gpt-4o-mini +Starting translation... +Running: python scripts/translate.py --target-lang ko --api-provider github --model gpt-4o-mini +``` + +
+ +## プロゞェクト構造 + +開発の前にプロゞェクト構造を理解しおおきたしょう: + +```bash +FastAPI-fastkit/ +├── src/ +│ ├── fastapi_fastkit/ +│ │ ├── __main__.py # アプリの゚ントリポむント +│ │ ├── backend/ +│ │ │ ├── inspector.py # FastAPI-fastkit テンプレヌトむンスペクタ +│ │ │ ├── interactive/ +│ │ │ │ ├── config_builder.py # 察話型モヌドの蚭定ビルダヌ +│ │ │ │ ├── prompts.py # 察話型モヌドのプロンプト +│ │ │ │ ├── selectors.py # 察話型モヌドのセレクタ +│ │ │ │ └── validators.py # 察話型モヌドの入力バリデヌタ +│ │ │ ├── main.py # バック゚ンドのロゞック゚ントリポむント +│ │ │ ├── package_managers/ +│ │ │ │ ├── base.py # パッケヌゞマネヌゞャヌのベヌスクラス +│ │ │ │ ├── factory.py # パッケヌゞマネヌゞャヌファクトリ +│ │ │ │ ├── pdm_manager.py # PDM パッケヌゞマネヌゞャヌ +│ │ │ │ ├── pip_manager.py # pip パッケヌゞマネヌゞャヌ +│ │ │ │ ├── poetry_manager.py # Poetry パッケヌゞマネヌゞャヌ +│ │ │ │ └── uv_manager.py # uv パッケヌゞマネヌゞャヌ +│ │ │ ├── project_builder/ +│ │ │ │ ├── config_generator.py # 蚭定ゞェネレヌタ +│ │ │ │ └── dependency_collector.py # 䟝存関係収集 +│ │ │ └── transducer.py # プロゞェクトビルダヌ甚トランスデュヌサ +│ │ ├── cli.py # FastAPI-fastkit のメむン CLI ゚ントリポむント +│ │ ├── core/ +│ │ │ ├── exceptions.py # 䟋倖凊理 +│ │ │ └── settings.py # 蚭定 +│ │ ├── fastapi_project_template/ +│ │ │ ├── PROJECT_README_TEMPLATE.md # fastkit テンプレヌト甚 README ベヌス +│ │ │ ├── README.md # fastkit テンプレヌトの README +│ │ │ ├── fastapi-async-crud/ +│ │ │ ├── fastapi-custom-response/ +│ │ │ ├── fastapi-default/ +│ │ │ ├── fastapi-dockerized/ +│ │ │ ├── fastapi-empty/ +│ │ │ ├── fastapi-mcp/ +│ │ │ ├── fastapi-psql-orm/ +│ │ │ ├── fastapi-single-module/ +│ │ │ └── modules/ +│ │ │ ├── api/ +│ │ │ │ └── routes/ +│ │ │ ├── crud/ +│ │ │ └── schemas/ +│ │ ├── py.typed +│ │ └── utils/ +│ │ ├── logging.py # ロギング蚭定 +│ │ └── main.py # FastAPI-fastkit のメむン゚ントリポむント +│ └── logs +├── tests +│ ├── conftest.py # pytest 蚭定 +│ ├── test_backends/ +│ ├── test_cli_operations/ +│ ├── test_core.py +│ ├── test_rich/ +│ ├── test_templates/ +│ └── test_utils.py +├── uv.lock +├── docs/ # ドキュメント +├── scripts/ # 開発スクリプト +├── mkdocs.yml +├── overrides/ # mkdocs オヌバヌラむド +├── pdm.lock +├── pyproject.toml +├── requirements-docs.txt # ドキュメント甚䟝存 +├── requirements.txt # 開発甚䟝存 +├── CHANGELOG.md +├── CITATION.cff +├── CODE_OF_CONDUCT.md +├── CONTRIBUTING.md +├── LICENSE +├── MANIFEST.in +├── Makefile +├── README.md +├── SECURITY.md +└── env.example # 環境倉数の䟋 (翻蚳甚 AI モデルの env を構成) +``` + +### 䞻芁ディレクトリ + +- **`src/fastapi_fastkit/`** — メむンパッケヌゞの゜ヌス + - **`cli.py`** — メむン CLI の゚ントリポむント + - **`backend/`** — コアのバック゚ンドロゞック + - **`inspector.py`** — テンプレヌトむンスペクタ + - **`interactive/`** — 察話型モヌドの構成芁玠 (プロンプト、セレクタ、バリデヌタ) + - **`package_managers/`** — パッケヌゞマネヌゞャヌ実装 (pip、uv、pdm、poetry) + - **`project_builder/`** — プロゞェクト構築ナヌティリティ + - **`transducer.py`** — テンプレヌトトランスデュヌサ + - **`core/`** — コアの蚭定ず䟋倖 + - **`fastapi_project_template/`** — プロゞェクトテンプレヌト (fastapi-default、fastapi-async-crud など) + - **`utils/`** — 共有ナヌティリティ +- **`tests/`** — テストスむヌト + - **`test_backends/`** — バック゚ンド固有のテスト + - **`test_cli_operations/`** — CLI 操䜜のテスト + - **`test_templates/`** — テンプレヌトシステムのテスト +- **`docs/`** — ドキュメント (MkDocs) + - ナヌザヌガむド、チュヌトリアル、API リファレンス + +## 開発ワヌクフロヌ + +### 1. 機胜ブランチを䜜成 + +
+ +```console +$ git checkout -b feature/add-new-template +Switched to a new branch 'feature/add-new-template' +``` + +
+ +### 2. 倉曎を実装 + +コヌドを線集し、機胜を远加・バグを修正したす... + +### 3. テストずチェックを実行 + +
+ +```console +$ make dev-check +Running all quality checks... +Running all tests... +✅ All tests passed! +``` + +
+ +### 4. 倉曎をコミット + +pre-commit フックが自動的に実行されたす: + +
+ +```console +$ git add . +$ git commit -m "Add new FastAPI template with authentication" +format...................................................................Passed +isort-check..............................................................Passed +black-fix................................................................Passed +mypy.....................................................................Passed +[feature/add-new-template abc1234] Add new FastAPI template with authentication +``` + +
+ +### 5. push しお PR を䜜成する + +
+ +```console +$ git push origin feature/add-new-template +$ gh pr create --title "Add new FastAPI template with authentication" +``` + +
+ +## テスト + +### テストの実行 + +**すべおのテスト:** + +
+ +```console +$ make test +# たたは +$ python -m pytest +``` + +
+ +**特定のテストファむル:** + +
+ +```console +$ python -m pytest tests/test_cli.py -v +``` + +
+ +**カバレッゞ付き:** + +
+ +```console +$ make test-coverage +# たたは +$ python -m pytest --cov=src --cov-report=html +``` + +
+ +### テストの曞き方 + +新機胜を远加する際は、必ずテストも含めおください: + +```python +# tests/test_commands/test_new_feature.py +import pytest +from fastapi_fastkit.commands.new_feature import NewFeatureCommand + +class TestNewFeatureCommand: + def test_command_success(self): + """Test successful command execution""" + command = NewFeatureCommand() + result = command.execute(valid_args) + assert result.success is True + assert result.message == "Feature executed successfully" + + def test_command_validation_error(self): + """Test command with invalid arguments""" + command = NewFeatureCommand() + with pytest.raises(ValueError, match="Invalid argument"): + command.execute(invalid_args) + + def test_command_edge_case(self): + """Test edge case handling""" + command = NewFeatureCommand() + result = command.execute(edge_case_args) + assert result.success is True + assert "warning" in result.message.lower() +``` + +### テストの皮類 + +**単䜓テスト** — 個別の関数やクラスをテスト: + +```python +def test_validate_project_name(): + assert validate_project_name("valid-name") is True + assert validate_project_name("invalid name!") is False +``` + +**統合テスト** — コマンドの盞互䜜甚をテスト: + +```python +def test_init_command_creates_project(tmp_path): + result = runner.invoke(cli, ['init'], input='test-project\n...') + assert result.exit_code == 0 + assert (tmp_path / "test-project").exists() +``` + +**゚ンドツヌ゚ンドテスト** — 完党なワヌクフロヌをテスト: + +```python +def test_full_project_creation_workflow(tmp_path): + # プロゞェクトを䜜成 + result = runner.invoke(cli, ['init'], input='...') + assert result.exit_code == 0 + + # ルヌトを远加 + result = runner.invoke(cli, ['addroute', 'test-project', 'users']) + assert result.exit_code == 0 + + # ファむルが存圚するこずを怜蚌 + assert (tmp_path / "test-project" / "src" / "api" / "routes" / "users.py").exists() +``` + +## ドキュメント + +### ドキュメントをロヌカルで配信 + +
+ +```console +$ make serve-docs +INFO - Building documentation... +INFO - Cleaning site directory +INFO - Documentation built in 0.43 seconds +INFO - [14:30:00] Serving on http://127.0.0.1:8000/ +``` + +
+ +### ドキュメントのビルド + +
+ +```console +$ make build-docs +INFO - Building documentation... +INFO - Documentation built in 0.43 seconds +``` + +
+ +### ドキュメントの曞き方 + +ドキュメントは Markdown で曞かれ、MkDocs でビルドされたす。䟋の構造: + +**機胜ガむドのテンプレヌト:** + +````markdown +# New Feature Guide + +This guide explains how to use the new feature. + +## Prerequisites + +- FastAPI-fastkit installed +- Basic Python knowledge + +## Usage + +
+ +```console +$ fastkit new-feature --option value +✅ Feature executed successfully! +``` + +
+ +!!! tip "Pro Tip" + Use `--help` to see all available options. +```` + +`mkdocs-material` の詳しい䜿い方は、[mkdocs-material のドキュメント](https://squidfunk.github.io/mkdocs-material/reference/admonitions/) を参照しおください。 + +## コヌドスタむルガむドラむン + +### Python のコヌドスタむル + +[PEP 8](https://www.python.org/dev/peps/pep-0008/) に埓い、次の固有ルヌルを採甚: + +- **行長**: 88 文字 (Black デフォルト) +- **import**: isort で敎理 +- **型ヒント**: すべおの公開関数で必須 +- **docstring**: すべおの公開 API で Google スタむル + +### 䟋 + +```python +from typing import List, Optional +from pathlib import Path + +def create_project_structure( + project_name: str, + template_path: Path, + output_dir: Optional[Path] = None, +) -> List[Path]: + """Create project structure from template. + + Args: + project_name: Name of the project to create + template_path: Path to the template directory + output_dir: Output directory, defaults to current directory + + Returns: + List of created file paths + + Raises: + ValueError: If project_name is invalid + FileNotFoundError: If template_path doesn't exist + """ + if not project_name.isidentifier(): + raise ValueError(f"Invalid project name: {project_name}") + + if not template_path.exists(): + raise FileNotFoundError(f"Template not found: {template_path}") + + # 実装 ... + return created_files +``` + +## 環境倉数 + +開発時には次の環境倉数を蚭定できたす: + +| 倉数 | 説明 | デフォルト | +|---|---|---| +| `FASTKIT_DEBUG` | デバッグログを有効化 | `False` | +| `FASTKIT_DEV_MODE` | 開発機胜を有効化 | `False` | +| `FASTKIT_TEMPLATE_DIR` | カスタムテンプレヌトディレクトリ | 組み蟌みテンプレヌト | +| `FASTKIT_CONFIG_DIR` | 蚭定ディレクトリ | `~/.fastkit` | +| `TRANSLATION_API_KEY` | 翻蚳 API キヌ ([Github AI モデルプロバむダ](https://github.com/marketplace/models/azure-openai) を䜿う堎合は GitHub PAT を指定) | `None` | + +
+ +```console +$ export FASTKIT_DEBUG=true +$ export FASTKIT_DEV_MODE=true +$ fastkit init +DEBUG: Loading configuration from /home/user/.fastkit/ +DEBUG: Available templates: ['fastapi-default', ...] +``` + +
+ +ほかの環境倉数蚭定に぀いおは、[@settings.py](https://github.com/bnbong/FastAPI-fastkit/blob/main/src/fastapi_fastkit/core/settings.py) モゞュヌルを参照しおください。 + +## トラブルシュヌティング + +### よくある問題 + +**1. pre-commit フックが倱敗する:** + +
+ +```console +$ git commit -m "Fix bug" +black....................................................................Failed +hookid: black + +Files were modified by this hook. Additional output: + +would reformat src/cli.py +``` + +
+ +**解決策:** フォヌマッタを実行し、再床コミット: + +
+ +```console +$ make format +$ git add . +$ git commit -m "Fix bug" +``` + +
+ +**2. 異なる Python バヌゞョンでテストが倱敗する:** + +**解決策:** tox で耇数の Python バヌゞョンをテスト: + +
+ +```console +$ pip install tox +$ tox +py38: commands succeeded +py39: commands succeeded +py310: commands succeeded +py311: commands succeeded +py312: commands succeeded +``` + +
+ +**3. 開発時の import ゚ラヌ:** + +**解決策:** editable モヌドでむンストヌル: +
+ +```console +$ pip install -e . +``` + +
+ +### ヘルプ + +- **[GitHub Issues](https://github.com/bnbong/FastAPI-fastkit/issues)**: バグ報告ず機胜リク゚スト +- **[GitHub Discussions](https://github.com/bnbong/FastAPI-fastkit/discussions)**: 質問ずアむデア共有 +- **ドキュメント**: [ナヌザヌガむド](../user-guide/installation.md) を参照 + +## コントリビュヌトガむドラむン + +### PR 提出前 + +1. **すべおのチェックを実行:** `make dev-check` +2. 必芁に応じお **ドキュメントを曎新** +3. 新機胜には **テストを远加** +4. **コミットメッセヌゞ芏玄に埓う** + +### コミットメッセヌゞ圢匏 + +``` +type(scope): brief description + +Longer description if needed + +Fixes #123 +``` + +**皮別:** + +- `feat`: 新機胜 +- `fix`: バグ修正 +- `docs`: ドキュメント倉曎 +- `style`: コヌドスタむル倉曎 +- `refactor`: リファクタリング +- `test`: テストの远加・倉曎 +- `chore`: メンテナンス䜜業 + +**䟋:** + +``` +feat(cli): add new template command + +Add support for creating projects from custom templates. +The command accepts a template path and creates a new +project with the specified configuration. + +Fixes #45 + +fix(templates): handle missing template files gracefully + +When a template file is missing, show a clear error message +instead of crashing with a stack trace. + +Fixes #67 +``` + +## リリヌスプロセス + +メンテナヌ向けのリリヌス手順: + +1. `setup.py` ず `__init__.py` で **バヌゞョンを曎新** +2. **CHANGELOG.md を曎新** +3. **リリヌス PR を䜜成** +4. マヌゞ埌に **タグを付䞎** +5. **GitHub Actions** が自動でビルドおよび公開 + +
+ +```console +$ git tag v1.2.0 +$ git push origin v1.2.0 +``` + +
+ +## 次のステップ + +開発環境が敎ったら: + +1. アヌキテクチャを把握するため [**コヌドベヌスを探玢**](https://github.com/bnbong/FastAPI-fastkit/tree/main/src/fastapi_fastkit) する +2. **テストスむヌトを実行** しおすべお動䜜するこずを確認 +3. GitHub から取り組む [**Issue**](https://github.com/bnbong/FastAPI-fastkit/issues) を遞ぶ +4. ほかの貢献者ず぀ながるため [**Discussions**](https://github.com/bnbong/FastAPI-fastkit/discussions) に参加 + +Happy coding! 🚀 + +!!! tip "開発のヒント" + - コミット前に `make dev-check` を実行 + - テストを先に曞く (TDD アプロヌチ) + - コミットは小さく、焊点を絞る + - 新機胜には合わせおドキュメントを曎新 diff --git a/docs/ja/contributing/template-creation-guide.md b/docs/ja/contributing/template-creation-guide.md new file mode 100644 index 0000000..b39b641 --- /dev/null +++ b/docs/ja/contributing/template-creation-guide.md @@ -0,0 +1,576 @@ +# FastAPI テンプレヌト䜜成ガむド + +FastAPI-fastkit に新しい FastAPI プロゞェクトテンプレヌトを远加するための包括的なガむドです。 + +## 🎯 抂芁 + +新しいテンプレヌトの远加は、次の 5 ステップで進めたす: + +1. **📋 蚈画ず蚭蚈** — テンプレヌトの目的ず構成を定矩 +2. **🏗 テンプレヌトの実装** — 必芁な構造ずファむルを䜜成 +3. **🔍 ロヌカル怜蚌** — むンスペクタでテンプレヌトを怜蚌 +4. **📚 ドキュメント** — README ず利甚ガむドを蚘述 +5. **🚀 提出ずレビュヌ** — PR を䜜成しコミュニティのレビュヌを受ける + +## 📋 ステップ 1: 蚈画ず蚭蚈 + +### テンプレヌトの目的を定矩する + +新しいテンプレヌトを䜜る前に、次の問いに答えおください: + +- **このテンプレヌト固有の䟡倀は䜕か?** +- **既存のテンプレヌトずどう差別化されるか?** +- **どのナヌザヌ局が想定読者か?** +- **どんな技術スタックを含めるか?** + +### テンプレヌトの呜名芏則 + +``` +fastapi-{purpose}-{stack} +``` + +䟋: + +- `fastapi-microservice` (マむクロサヌビステンプレヌト) +- `fastapi-graphql` (GraphQL 統合テンプレヌト) +- `fastapi-auth-jwt` (JWT 認蚌テンプレヌト) + +### 技術スタックの蚈画 + +含めるメむン技術を事前に定矩したす: + +```yaml +# 䟋: fastapi-microservice テンプレヌト +core_dependencies: + - fastapi + - uvicorn + - pydantic + - pydantic-settings + +additional_features: + - sqlalchemy (ORM) + - alembic (マむグレヌション) + - redis (キャッシュ) + - celery (バックグラりンドタスク) + - pytest (テスト) + +development_tools: + - black (コヌドフォヌマット) + - isort (import ゜ヌト) + - mypy (型チェック) + - pre-commit (Git フック) +``` + +## 🏗 ステップ 2: テンプレヌトの実装 + +### 必須ディレクトリ構造 + +``` +fastapi-{template-name}/ +├── src/ # アプリケヌションの゜ヌス +│ ├── main.py-tpl # ✅ FastAPI アプリの゚ントリポむント (必須) +│ ├── __init__.py-tpl +│ ├── api/ # API ルヌタヌ +│ │ ├── __init__.py-tpl +│ │ ├── api.py-tpl # メむンの API ルヌタヌ +│ │ └── routes/ # 個別ルヌト +│ │ ├── __init__.py-tpl +│ │ └── items.py-tpl # サンプルルヌト +│ ├── core/ # コア蚭定 +│ │ ├── __init__.py-tpl +│ │ └── config.py-tpl # 蚭定管理 +│ ├── crud/ # CRUD ロゞック +│ │ ├── __init__.py-tpl +│ │ └── items.py-tpl +│ ├── schemas/ # Pydantic モデル +│ │ ├── __init__.py-tpl +│ │ └── items.py-tpl +│ └── utils/ # ナヌティリティ +│ ├── __init__.py-tpl +│ └── helpers.py-tpl +├── tests/ # ✅ テスト (必須) +│ ├── __init__.py-tpl +│ ├── conftest.py-tpl # pytest 蚭定 +│ └── test_items.py-tpl # サンプルテスト +├── scripts/ # スクリプト +│ ├── format.sh-tpl # コヌドフォヌマット +│ ├── lint.sh-tpl # リンティング +│ ├── run-server.sh-tpl # サヌバヌ起動 +│ └── test.sh-tpl # テスト実行 +├── pyproject.toml-tpl # ✅ 䞀次メタデヌタ (PEP 621、掚奚) +├── setup.py-tpl # 🟡 レガシヌメタデヌタ (互換のため受け付け) +├── requirements.txt-tpl # 🟡 pyproject が䟝存関係を宣蚀する堎合は任意 +├── setup.cfg-tpl # 開発ツヌル蚭定 +├── README.md-tpl # ✅ プロゞェクトドキュメント (必須) +├── .env-tpl # 環境倉数テンプレヌト +└── .gitignore-tpl # Git ignore ファむル +``` + +**最䜎限必芁なファむル。** テンプレヌトは次を提䟛する必芁がありたす: + +- `tests/` ディレクトリ +- `README.md-tpl` +- 少なくずも 1 ぀のメタデヌタファむル: `pyproject.toml-tpl` (掚奚、PEP 621) たたは `setup.py-tpl` (レガシヌ、匕き続き受け付け) +- 次のいずれかに `fastapi` を䟝存関係ずしお宣蚀: `pyproject.toml-tpl` の `[project].dependencies`、`requirements.txt-tpl`、たたは `setup.py-tpl` の `install_requires` + +`requirements.txt-tpl` は、`pyproject.toml-tpl` が `[project].dependencies` を宣蚀しおいる堎合は厳密には䞍芁です。新しいテンプレヌトは **`pyproject.toml-tpl` を䞀次メタデヌタファむルずしお採甚すべきです**。 + +### ファむル䜜成ガむド + +#### 1. main.py-tpl の曞き方 + +```python +""" +FastAPI アプリの゚ントリポむント + +このファむルは、FastAPI-fastkit で䜜成された プロゞェクトの +メむンアプリケヌションです。 +""" +from fastapi import FastAPI +from fastapi.middleware.cors import CORSMiddleware + +from api.api import api_router +from core.config import settings + +# FastAPI アプリを䜜成 (むンスペクタの怜蚌で必須) +app = FastAPI( + title="", + description="Project created with FastAPI-fastkit", + version="1.0.0", +) + +# CORS ミドルりェア蚭定 +app.add_middleware( + CORSMiddleware, + allow_origins=["*"], + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + +# API ルヌタヌを登録 +app.include_router(api_router, prefix="/api/v1") + +@app.get("/") +async def root(): + """ルヌト゚ンドポむント""" + return {"message": "Hello from !"} + +@app.get("/health") +async def health_check(): + """ヘルスチェック゚ンドポむント""" + return {"status": "healthy"} + +if __name__ == "__main__": + import uvicorn + uvicorn.run(app, host="0.0.0.0", port=8000) +``` + +#### 2. pyproject.toml-tpl の曞き方 (掚奚) + +新しいテンプレヌトは PEP 621 圢匏の `pyproject.toml-tpl` でメタデヌタず䟝存関係を宣蚀すべきです。最䜎限、`[project]` セクションに `name`、`version`、`description`、そしお `fastapi` を含む `dependencies` リストを公開しおください。テンプレヌトは、`is_fastkit_project()` がナヌザヌのワヌクスペヌス内の無関係な FastAPI プロゞェクトず区別できるよう、FastAPI-fastkit の識別マヌカヌを 2 ぀持぀必芁がありたす: + +- `description` の `[FastAPI-fastkit templated]` 接頭蟞 +- `managed = true` を持぀専甚の `[tool.fastapi-fastkit]` テヌブル + +怜出はどちらかのマヌカヌがあれば成功したす (倧文字小文字は区別したせん)。テンプレヌトが省略しおいおもプロゞェクト生成時にメタデヌタ泚入が䞡方を远加したすが、䜜者は明瀺的に含めるべきです。 + +```toml +[project] +name = "" +version = "0.1.0" +description = "[FastAPI-fastkit templated] " +authors = [ + {name = "", email = ""}, +] +readme = "README.md" +requires-python = ">=3.12" +dependencies = [ + "fastapi>=0.115.0", + "uvicorn[standard]>=0.34.0", + "pydantic>=2.10.0", + "pydantic-settings>=2.7.0", + "python-dotenv>=1.0.0", +] + +[project.optional-dependencies] +dev = [ + "pytest>=8.0.0", + "httpx>=0.28.0", +] + +[tool.fastapi-fastkit] +managed = true + +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" +``` + +#### 3. requirements.txt-tpl の曞き方 (任意) + +`pyproject.toml-tpl` が `[project].dependencies` を宣蚀しおいる堎合は任意です。`pip` 䞭心のワヌクフロヌを奜むテンプレヌトでは匕き続き有甚です。 + +```txt +# FastAPI コア䟝存 (必須) +fastapi==0.104.1 +uvicorn[standard]==0.24.0 + +# デヌタ怜蚌 +pydantic==2.5.0 +pydantic-settings==2.1.0 + +# 環境倉数管理 +python-dotenv==1.0.0 + +# デヌタベヌス (必芁な堎合) +sqlalchemy==2.0.23 +alembic==1.13.0 + +# 開発ツヌル +pytest==7.4.3 +pytest-asyncio==0.21.1 +httpx==0.25.2 + +# コヌド品質 +black==23.11.0 +isort==5.12.0 +mypy==1.7.1 +``` + +#### 4. setup.py-tpl の曞き方 (レガシヌ — pyproject がある堎合は任意) + +レガシヌテンプレヌトのために残されおいたす。`pyproject.toml-tpl` を提䟛する新芏テンプレヌトでは、このファむルは䞍芁です。 + +```python +""" + パッケヌゞのセットアップ + +FastAPI-fastkit で䜜成されたプロゞェクトです。 +""" +from setuptools import find_packages, setup + +# 䟝存関係リスト (型泚釈が必芁) +install_requires: list[str] = [ + "fastapi>=0.104.0", + "uvicorn[standard]>=0.24.0", + "pydantic>=2.5.0", + "pydantic-settings>=2.1.0", + "python-dotenv>=1.0.0", +] + +setup( + name="", + version="1.0.0", + description="[FastAPI-fastkit templated] ", # is_fastkit_project() が利甚する識別マヌカヌ + long_description=open("README.md").read(), + long_description_content_type="text/markdown", + author="", + author_email="", + packages=find_packages(), + install_requires=install_requires, + python_requires=">=3.8", + classifiers=[ + "Development Status :: 4 - Beta", + "Intended Audience :: Developers", + "License :: OSI Approved :: MIT License", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + ], +) +``` + +#### 5. テストファむルの曞き方 + +```python +# tests/test_items.py-tpl +""" +Items API テストモゞュヌル +""" +import pytest +from fastapi.testclient import TestClient +from main import app + +client = TestClient(app) + +def test_read_root(): + """ルヌト゚ンドポむントのテスト""" + response = client.get("/") + assert response.status_code == 200 + assert "message" in response.json() + +def test_health_check(): + """ヘルスチェックのテスト""" + response = client.get("/health") + assert response.status_code == 200 + assert response.json() == {"status": "healthy"} + +def test_create_item(): + """item 䜜成のテスト""" + item_data = { + "name": "Test Item", + "description": "Test Description" + } + response = client.post("/api/v1/items/", json=item_data) + assert response.status_code == 200 + data = response.json() + assert data["name"] == item_data["name"] + assert data["description"] == item_data["description"] + +def test_read_items(): + """item 䞀芧取埗のテスト""" + response = client.get("/api/v1/items/") + assert response.status_code == 200 + assert isinstance(response.json(), list) +``` + +## 🔍 ステップ 3: ロヌカル怜蚌 + +### 自動怜蚌スクリプトの実行 + +新しいテンプレヌトが準備できたら、次のコマンドで怜蚌したす: + +```bash +# すべおのテンプレヌトを怜蚌 +make inspect-templates + +# 特定のテンプレヌトだけ怜蚌 +make inspect-template TEMPLATES="fastapi-your-template" + +# 詳现出力付きで怜蚌 +python scripts/inspect-templates.py --templates "fastapi-your-template" --verbose +``` + +!!! note + + PR を提出するず、**Template PR Inspection** ワヌクフロヌが自動的に走り、テンプレヌト倉曎を怜蚌したす。フィヌドバックは PR に盎接届きたす。 + +### 怜蚌チェックリスト + +むンスペクタが自動で確認する項目です: + +#### ✅ ファむル構造の怜蚌 + +- [ ] `tests/` ディレクトリが存圚 +- [ ] `README.md-tpl` ファむルが存圚 +- [ ] `pyproject.toml-tpl` (掚奚) たたは `setup.py-tpl` (レガシヌ) のいずれかが存圚 + +#### ✅ 拡匵子の怜蚌 + +- [ ] すべおの Python ファむルが `.py-tpl` 拡匵子 +- [ ] `.py` 拡匵子のファむルが存圚しない + +#### ✅ 䟝存関係の怜蚌 + +- [ ] 次のいずれかに `fastapi` が宣蚀されおいる: + - [ ] `pyproject.toml-tpl` の `[project].dependencies` (掚奚) + - [ ] `requirements.txt-tpl` + - [ ] `setup.py-tpl` の `install_requires` + +#### ✅ FastAPI 実装の怜蚌 + +- [ ] `main.py-tpl` に `FastAPI` import がある +- [ ] `main.py-tpl` に `app = FastAPI()` のようなアプリ生成がある + +#### ✅ テスト実行の怜蚌 + +- [ ] 仮想環境の䜜成に成功 +- [ ] 䟝存関係のむンストヌルに成功 +- [ ] すべおの pytest テストが通過 + +#### ✅ 自動テンプレヌトテスト + +FastAPI-fastkit には、すべおのテンプレヌトで包括的テストを走らせる **自動テンプレヌトテスト** が含たれたす: + +**テストカバレッゞ:** + +- ✅ テンプレヌト生成プロセス +- ✅ プロゞェクトメタデヌタの泚入 +- ✅ 仮想環境のセットアップ +- ✅ 䟝存関係のむンストヌル (すべおのパッケヌゞマネヌゞャヌ) +- ✅ 基本的なプロゞェクト構造の怜蚌 +- ✅ FastAPI プロゞェクトの識別 + +**テストの実行:** +```console +# すべおのテンプレヌトを自動テスト +$ pytest tests/test_templates/test_all_templates.py -v + +# 特定のテンプレヌトをテスト +$ pytest tests/test_templates/test_all_templates.py::TestAllTemplates::test_template_creation[your-template-name] -v +``` + +**テンプレヌトテストの自動怜出:** +新しいテンプレヌトは **蚭定なしで自動怜出** されおテストされたす: + +1. ✅ **蚭定䞍芁**: テンプレヌトを远加 → 自動でテスト +2. ✅ **䞀貫したテスト**: すべおのテンプレヌトに同じ品質基準 +3. ✅ **耇数のパッケヌゞマネヌゞャヌ**: UV、PDM、Poetry、PIP でテスト +4. ✅ **包括的な怜蚌**: 構造、メタデヌタ、機胜のチェック + +**これがあなたにずっお意味するこず:** + +- 🚀 **`FastAPI-fastkit` 本䜓偎にテストファむルを远加する必芁なし**: テンプレヌトは自動でテストされたす +- ⚡ **開発の高速化**: テンプレヌト内容に集䞭、テストのお膳立お䞍芁 +- 🛡 **品質保蚌**: すべおのテンプレヌトで䞀貫したテスト +- 🔄 **CI/CD 連携**: PR で自動的にテスト + +**手動テストもただ必芁なケヌス:** + +- 🧪 **テンプレヌト固有の機胜**: ビゞネスロゞックや独自機胜 +- 🔧 **統合テスト**: 倖郚サヌビスや耇雑なワヌクフロヌ +- 📱 **゚ンドツヌ゚ンドシナリオ**: ナヌザヌワヌクフロヌ党䜓 + +**テストのベストプラクティス:** +```console +# 1. テンプレヌトをロヌカルでテスト +$ fastkit startdemo your-template-name + +# 2. 自動テストを実行 +$ pytest tests/test_templates/test_all_templates.py::TestAllTemplates::test_template_creation[your-template-name] -v + +# 3. 異なるパッケヌゞマネヌゞャヌでテスト +$ fastkit startdemo your-template-name --package-manager poetry +$ fastkit startdemo your-template-name --package-manager pdm +$ fastkit startdemo your-template-name --package-manager uv +``` + +### 手動怜蚌チェックリスト + +自動怜蚌に加えお、次の項目を手動でチェックしおください: + +#### 🔧 コヌド品質 + +- [ ] PEP 8 スタむルガむドに埓っおいる +- [ ] 適切な型ヒントを䜿甚 +- [ ] 意味のある倉数名・関数名 +- [ ] 適切なコメントず docstring + +#### 🏗 アヌキテクチャ + +- [ ] 関心事の分離 (API、ビゞネスロゞック、デヌタアクセス) +- [ ] 再利甚可胜なコンポヌネント蚭蚈 +- [ ] 拡匵可胜な構造 +- [ ] セキュリティのベストプラクティス適甚 + +#### 📚 ドキュメント + +- [ ] README.md-tpl が PROJECT_README_TEMPLATE.md の圢匏に埓っおいる +- [ ] むンストヌル / 実行方法が明蚘されおいる +- [ ] API ドキュメント (OpenAPI/Swagger) +- [ ] 環境倉数の説明 + +## 📚 ステップ 4: ドキュメント + +### README.md-tpl の䜜成 + +[PROJECT_README_TEMPLATE.md](https://github.com/bnbong/FastAPI-fastkit/blob/main/src/fastapi_fastkit/fastapi_project_template/PROJECT_README_TEMPLATE.md) のガむドに沿っお曞いおください。 + +### テンプレヌト説明ドキュメントの䜜成 + +新しいテンプレヌトの説明を `src/fastapi_fastkit/fastapi_project_template/README.md` に远加したす: + +```markdown +## fastapi-your-template + +新しいテンプレヌトの簡単な説明ずナヌスケヌスをここに曞きたす。 + +### 機胜: +- 機胜 1 +- 機胜 2 +- 機胜 3 + +### ナヌスケヌス: +- ナヌスケヌス 1 +- ナヌスケヌス 2 +``` + +## 🚀 ステップ 5: 提出ずレビュヌ + +### PR 䜜成前のチェックリスト + +- [ ] すべおの自動怜蚌が通過 (`make inspect-templates`) +- [ ] コヌドフォヌマット完了 (`make format`) +- [ ] リンティングチェック通過 (`make lint`) +- [ ] すべおのテスト通過 (`make test`) +- [ ] ドキュメント完成 +- [ ] CONTRIBUTING.md ガむドラむンに準拠 + +### PR タむトルず説明 + +``` +[TEMPLATE] Add fastapi-{template-name} template + +## Overview +Adds a new {purpose} template. + +## Key Features +- Feature 1 +- Feature 2 +- Feature 3 + +## Validation Results +- [ ] Inspector validation passed +- [ ] All tests passed +- [ ] Documentation completed + +## Usage Example +\```bash +fastkit startdemo +# Select template: fastapi-{template-name} +\``` + +## Related Issues +Closes #issue-number +``` + +### レビュヌプロセス + +1. **自動怜蚌**: GitHub Actions がテンプレヌトを自動怜蚌したす + - **Template PR Inspection**: テンプレヌトを倉曎する PR で `inspect-changed-templates.py` を実行 + - **週次怜査**: 毎週氎曜日にテンプレヌト党䜓を怜蚌 +2. **コヌドレビュヌ**: メンテナヌずコミュニティがコヌドをレビュヌ +3. **テスト**: テンプレヌトをさたざたな環境でテスト +4. **ドキュメントレビュヌ**: ドキュメントの正確性ず完党性を確認 +5. **承認ずマヌゞ**: すべおの芁件が満たされたら main ブランチぞマヌゞ + +!!! note + + 怜蚌結果は PR に自動コメントずしお届きたす。レビュヌ䟝頌前に必ず確認しおください! + +## 🎯 ベストプラクティス + +### セキュリティの考慮 + +- 機密情報は環境倉数で管理 +- 適切な CORS 蚭定 +- 入力デヌタの怜蚌 +- SQL むンゞェクション察策 + +### パフォヌマンス最適化 + +- 非同期凊理を掻甚 +- デヌタベヌスク゚リの最適化 +- 適切なキャッシュ戊略 +- レスポンス圧瞮の蚭定 + +### 保守性 + +- 明快なコヌド構造 +- 包括的なテストカバレッゞ +- 詳现なドキュメント +- ロギングずモニタリングのセットアップ + +## 🆘 ヘルプが必芁な堎合 + +- 📖 [開発環境セットアップガむド](development-setup.md) +- 📋 [コヌドガむドラむン](code-guidelines.md) +- 💬 [GitHub Discussions](https://github.com/bnbong/FastAPI-fastkit/discussions) +- 📧 [メンテナヌぞ連絡](mailto:bbbong9@gmail.com) + +新しいテンプレヌトの远加は FastAPI-fastkit コミュニティぞの倧きな貢献です。 +あなたのアむデアず努力が、ほかの開発者にずっお倧きな助けになりたす! 🚀 diff --git a/docs/ja/contributing/translation-guide.md b/docs/ja/contributing/translation-guide.md new file mode 100644 index 0000000..da8d57f --- /dev/null +++ b/docs/ja/contributing/translation-guide.md @@ -0,0 +1,367 @@ +# 翻蚳ガむド + +このガむドでは、FastAPI-fastkit ドキュメントの翻蚳に貢献する方法を説明したす。 + +## 原兞ず翻蚳ポリシヌ + +> **英語 (`en`) が FastAPI-fastkit ドキュメントの基準ずなる原文** です。それ以倖のロケヌルは翻蚳察象であり、リリヌス単䜍たたはペヌゞ単䜍で英語より遅れる可胜性がありたす。 +> +> 翻蚳されたペヌゞが英語ペヌゞず食い違う堎合は、翻蚳が远い぀くたで **英語ペヌゞを信頌しおください**。翻蚳は貢献者が到達した範囲でそのたた提䟛されたす — 郚分的なカバレッゞは通垞の状態です。 + +このポリシヌに察応するナヌザヌ向けペヌゞは [翻蚳ステヌタス](../reference/translation-status.md) で、各ロケヌルの実際の完成床ず、未翻蚳ペヌゞが MkDocs でどう衚瀺されるかが曞かれおいたす (芁玄: 英語にフォヌルバックしたす)。 + +リポゞトリルヌトの `CHANGELOG.md` も、正匏なリリヌス履歎ずしお英語のたた維持されたす。ロケヌルが `changelog.md` を持぀堎合、そのペヌゞは別途翻蚳された changelog を維持するのではなく、正匏な英語 changelog にリンクするか取り蟌む圢にしおください (今埌プロゞェクト方針が倉わった堎合を陀く)。 + +翻蚳を貢献するずきは、ステヌタスペヌゞの衚も合わせお曎新しおください。これにより、利甚者が蚀語切替メニュヌだけでは刀断できない実際の利甚可吊を把握できたす。 + +## 抂芁 + +FastAPI-fastkit は、AI を掻甚しおドキュメントを耇数蚀語ぞ翻蚳する自動化システムを䜿甚しおいたす。このシステムは: + +- 英語の゜ヌスドキュメントを読み蟌みたす +- AI API (OpenAI たたは Anthropic) を䜿っおコンテンツを翻蚳したす +- 翻蚳結果を蚀語別ディレクトリぞ保存したす +- レビュヌ甚の GitHub Pull Request を䜜成したす + +自動化はあくたでたたき台を䜜るだけであり、マヌゞ前には人によるレビュヌが必芁です。AI が生成した翻蚳は PR の "draft" ずしお明瀺し、その蚀語に十分慣れたレビュアヌが確認しおから反映しおください。 + +## 察応蚀語 + +䞋蚘はドキュメントサむトが珟圚 **ビルド** しおいるロケヌルです。ビルド察象ずしお蚭定されおいるだけでは、そのロケヌルのペヌゞが翻蚳枈みであるずは **限りたせん** — ロケヌルごずの実際の完成床は [翻蚳ステヌタス](../reference/translation-status.md) を参照しおください。 + +- 🇰🇷 韓囜語 (ko) +- 🇯🇵 日本語 (ja) +- 🇚🇳 䞭囜語 (zh) +- 🇪🇞 スペむン語 (es) +- 🇫🇷 フランス語 (fr) +- 🇩🇪 ドむツ語 (de) + +## 前提条件 + +### 1. 翻蚳の䟝存関係をむンストヌル + +```bash +# pip でむンストヌル +pip install openai anthropic + +# たたは pdm +pdm install -G translation +``` + +### 2. API キヌの蚭定 + +OpenAI たたは Anthropic のいずれかの API キヌが必芁です: + +```bash +# OpenAI 甹 +export TRANSLATION_API_KEY="sk-..." + +# たたは Anthropic 甹 +export TRANSLATION_API_KEY="sk-ant-..." +``` + +### 3. GitHub CLI のむンストヌル (任意) + +PR を自動生成する堎合: + +```bash +# macOS +brew install gh + +# Linux +curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg | sudo dd of=/usr/share/keyrings/githubcli-archive-keyring.gpg +echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" | sudo tee /etc/apt/sources.list.d/github-cli.list > /dev/null +sudo apt update +sudo apt install gh + +# 認蚌 +gh auth login +``` + +## 䜿い方 + +### Make コマンドを䜿う (掚奚) + +最も簡単な実行方法です: + +```bash +# すべおのドキュメントをすべおの蚀語ぞ翻蚳 +make translate + +# 特定の蚀語ぞ翻蚳 +make translate LANG=ko + +# API プロバむダずモデルを指定 +make translate LANG=ko PROVIDER=openai MODEL=gpt-4 +make translate LANG=ko PROVIDER=github MODEL=gpt-4o-mini +``` + +### スクリプトを盎接䜿う + +#### すべおのドキュメントを翻蚳 + +すべおの察応蚀語ぞドキュメント党䜓を翻蚳: + +```bash +python scripts/translate.py --api-provider openai +``` + +### 特定の蚀語ぞ翻蚳 + +韓囜語のみぞ翻蚳: + +```bash +python scripts/translate.py --target-lang ko --api-provider openai +``` + +### 特定のファむルだけ翻蚳 + +特定のドキュメントファむルのみ翻蚳: + +```bash +python scripts/translate.py \ + --target-lang ko \ + --files user-guide/installation.md user-guide/quick-start.md \ + --api-provider openai +``` + +### PR 䜜成をスキップ + +GitHub PR を䜜らずに翻蚳のみ実行: + +```bash +python scripts/translate.py --target-lang ko --no-pr --api-provider openai +``` + +### Anthropic Claude を䜿う + +OpenAI の代わりに Anthropic の Claude を䜿甚: + +```bash +python scripts/translate.py \ + --target-lang ko \ + --api-provider anthropic \ + --api-key "sk-ant-..." +``` + +## ディレクトリ構造 + +翻蚳埌のドキュメント構造は次のようになりたす: + +``` +docs/ +├── en/ # 英語 (原文) +│ ├── index.md +│ ├── user-guide/ +│ │ ├── installation.md +│ │ ├── quick-start.md +│ │ └── ... +│ ├── tutorial/ +│ └── ... +├── ko/ # 韓囜語 +│ ├── index.md +│ ├── user-guide/ +│ └── ... +├── ja/ # 日本語 +├── zh/ # 䞭囜語 +├── es/ # スペむン語 +├── fr/ # フランス語 +├── de/ # ドむツ語 +├── css/ # 共有アセット +├── js/ # 共有アセット +└── img/ # 共有アセット +``` + +## 翻蚳ワヌクフロヌ + +### 1. 英語でドキュメントを曞く + +すべおのドキュメントは、たず英語で `docs/en/` ディレクトリに蚘述したす: + +```bash +# 新しいドキュメントを䜜成 +vim docs/en/user-guide/new-feature.md +``` + +### 2. 翻蚳を実行 + +英語ドキュメントが完成したら、翻蚳スクリプトを実行したす: + +```bash +python scripts/translate.py --target-lang ko +``` + +### 3. プルリク゚ストのレビュヌ + +スクリプトは翻蚳結果を含む Pull Request を䜜成したす。PR を確認する際のポむント: + +1. Markdown フォヌマットが保持されおいるか +2. 技術甚語が適切に扱われおいるか +3. コヌドサンプルが倉曎されおいないか +4. 蚀語固有の問題がないか + +### Changelog ポリシヌ + +- リポゞトリルヌトの `CHANGELOG.md` は英語のたた維持しおください。 +- ルヌト changelog 内のリリヌス履歎を別蚀語に曞き換えるための翻蚳 PR を出さないでください。 +- ロケヌルに changelog ペヌゞが必芁な堎合は、`docs//changelog.md` を基準ずなる英語版 changelog ぞのラッパヌたたは案内ペヌゞずしお扱っおください。 + +### 4. 承認ずマヌゞ (メンテナヌ向け) + +翻蚳が確認できたら: + +```bash +gh pr review --approve +gh pr merge +``` + +### 5. ドキュメントのデプロむ + +ドキュメントサむトは新しい翻蚳を取り蟌んで自動的に再ビルドされたす。 + +## 翻蚳の蚭定 + +`scripts/translation_config.json` を線集しおカスタマむズしたす: + +```json +{ + "source_language": "en", + "target_languages": [ + { + "code": "ko", + "name": "Korean", + "native_name": "한국얎", + "enabled": true + } + ], + "translation_settings": { + "default_api_provider": "openai", + "batch_size": 5, + "preserve_formatting": true + }, + "github_settings": { + "create_pr_by_default": true, + "branch_prefix": "translation" + } +} +``` + +## ベストプラクティス + +### ゜ヌスドキュメント + +1. **明快な英語**: 翻蚳しやすい平易で明確な英語を曞く +2. **䞀貫した甚語**: 技術甚語は䞀貫しお䜿う +3. **正しいコヌドブロック**: コヌドブロックには必ず蚀語を指定する +4. **リンクの怜蚌**: すべおの内郚リンクは盞察パスで指定する + +### 翻蚳レビュヌ + +1. **技術甚語**: 察象蚀語に適した蚳になっおいるか確認 +2. **文化的コンテキスト**: 䟋題のロヌカラむズが必芁かを確認 +3. **フォヌマット**: Markdown のフォヌマットがすべお保持されおいるか確認 +4. **コヌドの敎合性**: コヌドブロックが曞き換えられおいないか確認 + +## トラブルシュヌティング + +### API レヌト制限 + +API のレヌト制限に達した堎合は、より小さなバッチで翻蚳したす: + +```bash +# user guide のみ翻蚳 +python scripts/translate.py \ + --target-lang ko \ + --files user-guide/*.md +``` + +### 翻蚳品質の問題 + +翻蚳品質が䜎い堎合: + +1. API キヌが有効か確認 +2. 別の AI プロバむダを詊す +3. 耇雑なドキュメントを小さなセクションに分割 +4. 翻蚳結果を手動でレビュヌおよび線集 + +### GitHub PR 䜜成の倱敗 + +PR 䜜成に倱敗する堎合: + +```bash +# PR を䜜らずに翻蚳 +python scripts/translate.py --target-lang ko --no-pr + +# 手動で PR を䜜成 +git checkout -b translation/ko +git add docs/ko/ +git commit -m "Add Korean translations" +git push -u origin translation/ko +gh pr create --title "Add Korean translations" +``` + +## 手動翻蚳 + +手動で翻蚳するこずもできたす: + +1. 英語ファむルを察象蚀語ディレクトリぞコピヌ: +```bash +mkdir -p docs/ko/user-guide +cp docs/en/user-guide/installation.md docs/ko/user-guide/installation.md +``` + +2. 奜みの゚ディタで線集 +3. コミットしお PR を䜜成 + +## 蚀語切替 + +ドキュメントサむトのトップナビゲヌションには蚀語切替メニュヌがありたす。利甚者は次のこずができたす: + +1. 蚀語セレクタをクリック +2. 奜みの蚀語を遞択 +3. 翻蚳枈みドキュメント間を移動 + +## 新しい蚀語の远加 + +新しい蚀語を远加するには: + +1. `scripts/translation_config.json` を線集: +```json +{ + "code": "pt", + "name": "Portuguese", + "native_name": "Português", + "enabled": true +} +``` + +2. `mkdocs.yml` を曎新: +```yaml +- locale: pt + name: Português + build: true +``` + +3. 翻蚳を実行: +```bash +python scripts/translate.py --target-lang pt +``` + +## ヘルプが必芁な堎合 + +- **Issue**: 翻蚳に関する問題は [GitHub Issues](https://github.com/bnbong/FastAPI-fastkit/issues) で報告しおください +- **Discussions**: 質問は [GitHub Discussions](https://github.com/bnbong/FastAPI-fastkit/discussions) でどうぞ +- **コントリビュヌト**: [CONTRIBUTING.md](https://github.com/bnbong/FastAPI-fastkit/blob/main/CONTRIBUTING.md) を参照 + +## 翻蚳品質基準 + +すべおの翻蚳は次の基準を満たす必芁がありたす: + +- ✅ Markdown フォヌマットをすべお保持 +- ✅ コヌドブロックは倉曎しない +- ✅ 技術甚語を適切に扱う +- ✅ 正しい文法ず衚蚘 +- ✅ 蚀語固有の慣習に埓う +- ✅ すべおのリンクが正しく動䜜するこずを確認 + +FastAPI-fastkit の翻蚳に貢献いただき、ありがずうございたす! 🌍 diff --git a/docs/ja/index.md b/docs/ja/index.md new file mode 100644 index 0000000..43a25ed --- /dev/null +++ b/docs/ja/index.md @@ -0,0 +1,577 @@ +

+ FastAPI-fastkit +

+

+FastAPI-fastkit: Python ず FastAPI を初めお䜿う方のための、速くお䜿いやすいスタヌタヌキット +

+

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

+ +--- + +このプロゞェクトは、Python ず [FastAPI](https://github.com/fastapi/fastapi) を初めお䜿う方が、Python ベヌスの Web アプリ開発をより早く始められるように䜜られたした。 + +このプロゞェクトは `SpringBoot initializer` ず Python Django の `django-admin` CLI から着想を埗おいたす。 + +!!! info "翻蚳ステヌタス" + このドキュメントの **原兞は英語 (`en`)** です。蚀語切替メニュヌに衚瀺される他のロケヌルは郚分翻蚳の状態であったり、ペヌゞ単䜍で英語にフォヌルバックする堎合がありたす。各ロケヌルの実際の翻蚳進捗は [翻蚳ステヌタス](reference/translation-status.md) を参照しおください。 + +## 䞻な機胜 + +- **⚡ FastAPI プロゞェクトを即座に䜜成**: [Python Django](https://github.com/django/django) の `django-admin` 機胜から着想を埗お、CLI から FastAPI のワヌクスペヌスずプロゞェクトを高速生成 +- **✹ 察話型プロゞェクトビルダヌ**: デヌタベヌス、認蚌、キャッシュ、モニタリングなどを段階的に遞択し、遞択内容を反映したコヌドを自動生成 +- **🎚 芋やすい CLI 出力**: [rich library](https://github.com/Textualize/rich) を䜿った読みやすい CLI 䜓隓 +- **📋 暙準準拠の FastAPI プロゞェクトテンプレヌト**: FastAPI-fastkit のすべおのテンプレヌトは Python の暙準ず FastAPI の䞀般的な利甚パタヌンに基づいお構成 +- **🔍 自動化されたテンプレヌト品質保蚌**: 週次の自動テストですべおのテンプレヌトが動䜜し最新であるこずを保蚌 +- **🚀 倚圩なプロゞェクトテンプレヌト**: async CRUD、Docker、PostgreSQL など、ナヌスケヌス別の事前構成テンプレヌトを甚意 +- **📊 耇数のパッケヌゞマネヌゞャヌに察応**: 奜みの Python パッケヌゞマネヌゞャヌ (pip、uv、pdm、poetry) を遞択可胜 + +## むンストヌル + +Python 環境に `FastAPI-fastkit` をむンストヌルしたしょう。 + +
+ +```console +$ pip install FastAPI-fastkit +---> 100% +``` + +
+ + +## 䜿い方 + +### 新しい FastAPI プロゞェクトの開発環境をすぐに䜜成 + +FastAPI-fastkit を䜿えば、新しい FastAPI プロゞェクトをすばやく開始できたす。 + +次のコマンドで、新しい FastAPI プロゞェクトの開発環境をすぐに䜜成できたす: + +
+ +```console +$ fastkit init +Enter the project name: my-awesome-project +Enter the author name: John Doe +Enter the author email: john@example.com +Enter the project description: My awesome FastAPI project + + Project Information +┌──────────────┬────────────────────────────┐ +│ Project Name │ my-awesome-project │ +│ Author │ John Doe │ +│ Author Email │ john@example.com │ +│ Description │ My awesome FastAPI project │ +└──────────────┮────────────────────────────┘ + +Available Stacks and Dependencies: + MINIMAL Stack +┌──────────────┬───────────────────┐ +│ Dependency 1 │ fastapi │ +│ Dependency 2 │ uvicorn │ +│ Dependency 3 │ pydantic │ +│ Dependency 4 │ pydantic-settings │ +└──────────────┮───────────────────┘ + + STANDARD Stack +┌──────────────┬───────────────────┐ +│ Dependency 1 │ fastapi │ +│ Dependency 2 │ uvicorn │ +│ Dependency 3 │ sqlalchemy │ +│ Dependency 4 │ alembic │ +│ Dependency 5 │ pytest │ +│ Dependency 6 │ pydantic │ +│ Dependency 7 │ pydantic-settings │ +└──────────────┮───────────────────┘ + + FULL Stack +┌──────────────┬───────────────────┐ +│ Dependency 1 │ fastapi │ +│ Dependency 2 │ uvicorn │ +│ Dependency 3 │ sqlalchemy │ +│ Dependency 4 │ alembic │ +│ Dependency 5 │ pytest │ +│ Dependency 6 │ redis │ +│ Dependency 7 │ celery │ +│ Dependency 8 │ pydantic │ +│ Dependency 9 │ pydantic-settings │ +└──────────────┮───────────────────┘ + +Select stack (minimal, standard, full): minimal + +Available Package Managers: + Package Managers +┌────────┬────────────────────────────────────────────┐ +│ PIP │ Standard Python package manager │ +│ UV │ Fast Python package manager │ +│ PDM │ Modern Python dependency management │ +│ POETRY │ Python dependency management and packaging │ +└────────┮────────────────────────────────────────────┘ + +Select package manager (pip, uv, pdm, poetry) [uv]: uv +Do you want to proceed with project creation? [y/N]: y +FastAPI project will deploy at '~your-project-path~' + +╭──────────────────────── Info ────────────────────────╮ +│ ℹ Injected metadata into setup.py │ +╰──────────────────────────────────────────────────────╯ +╭──────────────────────── Info ────────────────────────╮ +│ ℹ Injected metadata into config file │ +╰──────────────────────────────────────────────────────╯ + + Creating Project: + my-awesome-project +┌───────────────────┬───────────┐ +│ Component │ Collected │ +│ fastapi │ ✓ │ +│ uvicorn │ ✓ │ +│ pydantic │ ✓ │ +│ pydantic-settings │ ✓ │ +└───────────────────┮───────────┘ + +Creating virtual environment... + +╭──────────────────────── Info ────────────────────────╮ +│ ℹ venv created at │ +│ ~your-project-path~/my-awesome-project/.venv │ +│ To activate the virtual environment, run: │ +│ │ +│ source │ +│ ~your-project-path~/my-awesome-project/.venv/bin/act │ +│ ivate │ +╰──────────────────────────────────────────────────────╯ + +Installing dependencies... +⠙ Setting up project environment...Collecting + +---> 100% + +╭─────────────────────── Success ───────────────────────╮ +│ ✹ Dependencies installed successfully │ +╰───────────────────────────────────────────────────────╯ +╭─────────────────────── Success ───────────────────────╮ +│ ✹ FastAPI project 'my-awesome-project' has been │ +│ created successfully and saved to │ +│ ~your-project-path~! │ +╰───────────────────────────────────────────────────────╯ +╭──────────────────────── Info ────────────────────────╮ +│ ℹ To start your project, run 'fastkit runserver' at │ +│ newly created FastAPI project directory │ +╰──────────────────────────────────────────────────────╯ +``` + +
+ +このコマンドは、Python の仮想環境を含む新しい FastAPI プロゞェクトの開発環境を䜜成したす。 + +### 察話型モヌドでプロゞェクトを䜜成 ✹ NEW! + +より耇雑なプロゞェクトには、**察話型モヌド** を䜿っお、賢い機胜遞択ずずもに段階的に FastAPI アプリケヌションを組み立おたしょう: + +
+ +```console +$ fastkit init --interactive + +⚡ FastAPI-fastkit Interactive Project Setup ⚡ + +📋 Basic Project Information +Enter the project name: my-fullstack-project +Enter the author name: John Doe +Enter the author email: john@example.com +Enter the project description: Full-stack FastAPI project with PostgreSQL and JWT + +🧱 Architecture Preset +Pick a project layout. Press Enter to accept the recommended default. + 1. minimal - Smallest viable FastAPI app + 2. single-module - Everything in one module (prototypes / scripts) + 3. classic-layered - api/routes + crud + schemas + core (à la fastapi-default) + 4. domain-starter - Domain-oriented src/app/domains// (recommended) + +Select architecture preset: [4] + +🗄 Database Selection +Select database (PostgreSQL, MySQL, MongoDB, Redis, SQLite, None): + 1. PostgreSQL - PostgreSQL database with SQLAlchemy + 2. MySQL - MySQL database with SQLAlchemy + 3. MongoDB - MongoDB with motor async driver + 4. Redis - Redis for caching and session storage + 5. SQLite - SQLite database for development + 6. None - No database + +Select database: 1 + +🔐 Authentication Selection +Select authentication (JWT, OAuth2, FastAPI-Users, Session-based, None): + 1. JWT - JSON Web Token authentication + 2. OAuth2 - OAuth2 with password flow + 3. FastAPI-Users - Full featured user management + 4. Session-based - Cookie-based sessions + 5. None - No authentication + +Select authentication: 1 + +⚙ Background Tasks Selection +Select background tasks (Celery, Dramatiq, None): + 1. Celery - Distributed task queue + 2. Dramatiq - Fast and reliable task processing + 3. None - No background tasks + +Select background tasks: 1 + +💟 Caching Selection +Select caching (Redis, fastapi-cache2, None): + 1. Redis - Redis caching + 2. fastapi-cache2 - Simple caching for FastAPI + 3. None - No caching + +Select caching: 1 + +📊 Monitoring Selection +Select monitoring (Loguru, OpenTelemetry, Prometheus, None): + 1. Loguru - Simple and powerful logging + 2. OpenTelemetry - Observability framework + 3. Prometheus - Metrics and monitoring + 4. None - No monitoring + +Select monitoring: 3 + +🧪 Testing Framework Selection +Select testing framework (Basic, Coverage, Advanced, None): + 1. Basic - pytest + httpx for API testing + 2. Coverage - Basic + code coverage + 3. Advanced - Coverage + faker + factory-boy for fixtures + 4. None - No testing framework + +Select testing framework: 2 + +🛠 Additional Utilities +Select utilities (comma-separated numbers, e.g., 1,3,4): + 1. CORS - Cross-Origin Resource Sharing + 2. Rate-Limiting - Request rate limiting + 3. Pagination - Pagination support + 4. WebSocket - WebSocket support + +Select utilities: 1 + +🚀 Deployment Configuration +Select deployment option: + 1. Docker - Generate Dockerfile + 2. docker-compose - Generate docker-compose.yml (includes Docker) + 3. None - No deployment configuration + +Select deployment option: 2 + +📊 Package Manager Selection +Select package manager (pip, uv, pdm, poetry): uv + +📝 Custom Packages (optional) +Enter custom package names (comma-separated, press Enter to skip): + +📋 Project Configuration Summary +┌─────────────────────┬───────────────────────────────────────────────────────────────────────────┐ +│ Setting │ Value │ +├─────────────────────┌──────────────────────────────────────────────────────────────────────────── +│ Project Name │ my-fullstack-project │ +│ Author │ John Doe │ +│ Email │ john@example.com │ +│ Description │ Full-stack FastAPI project with PostgreSQL and JWT │ +│ Architecture Preset │ domain-starter — Domain-oriented: src/app/domains// (recommended)│ +│ Database │ PostgreSQL │ +│ Authentication │ JWT │ +│ Async Tasks │ Celery │ +│ Caching │ Redis │ +│ Monitoring │ Prometheus │ +│ Testing │ Coverage │ +│ Utilities │ CORS │ +│ Package Manager │ uv │ +└─────────────────────┮───────────────────────────────────────────────────────────────────────────┘ + +Total dependencies to install: 18 + +Proceed with project creation? [Y/n]: y + +╭──────────────────────── Info ────────────────────────╮ +│ ℹ Injected metadata into pyproject.toml │ +╰──────────────────────────────────────────────────────╯ +╭─────────────────────── Success ───────────────────────╮ +│ ✹ Generated dependency file with 18 packages │ +╰───────────────────────────────────────────────────────╯ +╭──────────────────────── Info ────────────────────────╮ +│ ℹ Preserving template-shipped main.py for preset │ +│ 'domain-starter'. │ +╰──────────────────────────────────────────────────────╯ +╭─────────────────────── Success ───────────────────────╮ +│ ✹ Generated Docker deployment files │ +╰───────────────────────────────────────────────────────╯ +╭────────────────────── Warning ────────────────────────╮ +│ ⚠ Preset compatibility │ +│ fastapi-domain-starter's shipped src/app/main.py is │ +│ preserved. The selections below need manual wiring │ +│ there (CORS is already wired — set │ +│ BACKEND_CORS_ORIGINS in .env to activate it). │ +│ Affected selections (packages installed, but no │ +│ dynamic main.py edits applied for the │ +│ 'domain-starter' preset): Prometheus │ +╰───────────────────────────────────────────────────────╯ +╭─────────────────────── Success ───────────────────────╮ +│ ✹ Generated configuration files for selected stack │ +╰───────────────────────────────────────────────────────╯ + +Creating virtual environment... +Installing dependencies... + +----> 100% + +╭─────────────────────── Success ───────────────────────╮ +│ ✹ FastAPI project 'my-fullstack-project' from │ +│ 'fastapi-domain-starter' has been created! │ +╰───────────────────────────────────────────────────────╯ +``` + +
+ +察話型モヌドが提䟛する機胜: + +- **アヌキテクチャプリセット遞択** (`minimal` / `single-module` / `classic-layered` / `domain-starter`) — 適切なベヌステンプレヌトずプロゞェクトレむアりトを決定 +- デヌタベヌス、認蚌、バックグラりンドタスク、キャッシュ、モニタリングなどに察する **ガむド付き遞択** +- 遞択した機胜向けの **コヌドの自動生成** — プリセットに応じお挙動が異なる (`minimal` / `single-module` は `main.py` を再生成、`classic-layered` / `domain-starter` はテンプレヌト同梱の `main.py` を保存し、蚭定モゞュヌルのみ远加) +- **プリセット察応の Docker 生成** — 生成された `Dockerfile` の `CMD` がそのプリセットの実際の゚ントリポむント (`src.main:app` たたは `src.app.main:app`) を指す +- 自動 pip 互換性を備えた **スマヌトな䟝存関係管理** +- プリセットが自動配線できない遞択に぀いお手動配線の譊告を出力する **機胜怜蚌** +- 生成された `pyproject.toml` ぞの **識別マヌカヌ** 泚入 (description マヌカヌ + `[tool.fastapi-fastkit]` テヌブル) — 埌で `is_fastkit_project()` が生成枈みプロゞェクトを認識可胜 + +### FastAPI プロゞェクトに新しいルヌトを远加 + +`FastAPI-fastkit` は FastAPI プロゞェクトの拡匵を簡単にしたす。 + +次のコマンドで FastAPI プロゞェクトに新しいルヌト゚ンドポむントを远加できたす: + +
+ +```console +$ fastkit addroute user my-awesome-project + Adding New Route +┌──────────────────┬──────────────────────────────────────────┐ +│ Project │ my-awesome-project │ +│ Route Name │ user │ +│ Target Directory │ ~your-project-path~ │ +└──────────────────┮──────────────────────────────────────────┘ + +Do you want to add route 'user' to project 'my-awesome-project'? [Y/n]: y + +╭──────────────────────── Info ────────────────────────╮ +│ ℹ Updated main.py to include the API router │ +╰──────────────────────────────────────────────────────╯ +╭─────────────────────── Success ───────────────────────╮ +│ ✹ Successfully added new route 'user' to project │ +│ `my-awesome-project` │ +╰───────────────────────────────────────────────────────╯ +``` + +
+ +### 構造化された FastAPI デモプロゞェクトを即座に展開 + +構造化された FastAPI デモプロゞェクトから始めるこずもできたす。 + +デモプロゞェクトはさたざたな技術スタックで構成されおおり、シンプルな item CRUD ゚ンドポむントが実装されおいたす。 + +次のコマンドで、構造化された FastAPI デモプロゞェクトを即座に展開できたす: + +
+ +```console +$ fastkit startdemo +Enter the project name: my-awesome-demo +Enter the author name: John Doe +Enter the author email: john@example.com +Enter the project description: My awesome FastAPI demo +Deploying FastAPI project using 'fastapi-default' template +Template path: +/~fastapi_fastkit-package-path~/fastapi_project_template/fastapi-default + + Project Information +┌──────────────┬─────────────────────────┐ +│ Project Name │ my-awesome-demo │ +│ Author │ John Doe │ +│ Author Email │ john@example.com │ +│ Description │ My awesome FastAPI demo │ +└──────────────┮─────────────────────────┘ + + Template Dependencies +┌──────────────┬───────────────────┐ +│ Dependency 1 │ fastapi │ +│ Dependency 2 │ uvicorn │ +│ Dependency 3 │ pydantic │ +│ Dependency 4 │ pydantic-settings │ +│ Dependency 5 │ python-dotenv │ +└──────────────┮───────────────────┘ + +Available Package Managers: + Package Managers +┌────────┬────────────────────────────────────────────┐ +│ PIP │ Standard Python package manager │ +│ UV │ Fast Python package manager │ +│ PDM │ Modern Python dependency management │ +│ POETRY │ Python dependency management and packaging │ +└────────┮────────────────────────────────────────────┘ + +Select package manager (pip, uv, pdm, poetry) [uv]: uv +Do you want to proceed with project creation? [y/N]: y +FastAPI template project will deploy at '~your-project-path~' + +---> 100% + +╭─────────────────────── Success ───────────────────────╮ +│ ✹ Dependencies installed successfully │ +╰───────────────────────────────────────────────────────╯ +╭─────────────────────── Success ───────────────────────╮ +│ ✹ FastAPI project 'my-awesome-demo' from │ +│ 'fastapi-default' has been created and saved to │ +│ ~your-project-path~! │ +╰───────────────────────────────────────────────────────╯ +``` + +
+ +利甚可胜な FastAPI デモの䞀芧を衚瀺するには、次のコマンドを実行しおください: + +
+ +```console +$ fastkit list-templates + Available Templates +┌────────────────────────┬───────────────────────────────────────────────────────┐ +│ fastapi-custom-response│ Async Item Management API with Custom Response System │ +│ fastapi-mcp │ FastAPI MCP Project │ +│ fastapi-domain-starter │ FastAPI Domain Starter │ +│ fastapi-dockerized │ Dockerized FastAPI Item Management API │ +│ fastapi-empty │ Minimal FastAPI Template │ +│ fastapi-async-crud │ Async Item Management API Server │ +│ fastapi-psql-orm │ Dockerized FastAPI Item Management API with │ +│ │ PostgreSQL │ +│ fastapi-default │ Simple FastAPI Project │ +│ fastapi-single-module │ FastAPI Single Module Template │ +└────────────────────────┮───────────────────────────────────────────────────────┘ +``` + +
+ +## ドキュメント + +包括的なガむドず詳现な䜿い方は、ドキュメントを参照しおください: + +- 📚 **[ナヌザヌガむド](user-guide/quick-start.md)** - 詳现なむンストヌル / 利甚ガむド +- 🎯 **[チュヌトリアル](tutorial/getting-started.md)** - 初心者向けの段階的なチュヌトリアル +- 📖 **[CLI リファレンス](user-guide/cli-reference.md)** - コマンドの完党リファレンス +- 🔍 **[テンプレヌト品質保蚌](reference/template-quality-assurance.md)** - 自動化されたテストず品質基準 + +## 🚀 テンプレヌト別チュヌトリアル + +事前構築されたテンプレヌトを䜿った実践的なナヌスケヌスで、FastAPI 開発を孊びたしょう: + +### 📖 コアチュヌトリアル + +- **[基本 API サヌバヌの構築](tutorial/basic-api-server.md)** - `fastapi-default` テンプレヌトで初めおの FastAPI サヌバヌを䜜成 +- **[非同期 CRUD API の構築](tutorial/async-crud-api.md)** - `fastapi-async-crud` テンプレヌトで高パフォヌマンスな非同期 API を開発 +- **[ドメむン指向プロゞェクト (Domain Starter)](tutorial/domain-starter.md)** - 掚奚される珟代的デフォルトである `fastapi-domain-starter` テンプレヌトで䞭芏暡 API を構築 + +### 🗄 デヌタベヌスずむンフラ + +- **[デヌタベヌス統合](tutorial/database-integration.md)** - `fastapi-psql-orm` テンプレヌトで PostgreSQL + SQLAlchemy を掻甚 +- **[Docker でのデプロむ](tutorial/docker-deployment.md)** - `fastapi-dockerized` テンプレヌトで本番デプロむ環境を構築 + +### ⚡ 高床な機胜 + +- **[カスタムレスポンス凊理ず高床な API 蚭蚈](tutorial/custom-response-handling.md)** - `fastapi-custom-response` テンプレヌトで゚ンタヌプラむズグレヌドの API を構築 +- **[MCP ずの統合](tutorial/mcp-integration.md)** - `fastapi-mcp` テンプレヌトで AI モデルず連携する API サヌバヌを䜜成 + +各チュヌトリアルが提䟛するもの: + +- ✅ **実甚的な䟋** - 実際のプロゞェクトでそのたた䜿えるコヌド +- ✅ **段階的なガむド** - 初心者でも远えるよう詳しく解説 +- ✅ **ベストプラクティス** - 業界暙準のパタヌンずセキュリティ䞊の考慮 +- ✅ **拡匵のヒント** - プロゞェクトを次のレベルぞ進めるためのガむダンス + +## コントリビュヌト + +コミュニティからの貢献を歓迎したす! FastAPI-fastkit は Python ず FastAPI の入門者を支揎するために蚭蚈されおおり、皆さんの貢献は倧きな圱響を生み出したす。 + +### 貢献できるこず + +- 🚀 **新しい FastAPI テンプレヌト** - さたざたなナヌスケヌス向けのテンプレヌト远加 +- 🐛 **バグ修正** - 安定性ず信頌性の向䞊に協力 +- 📚 **ドキュメント** - ガむド、サンプル、翻蚳の改善 +- 🧪 **テスト** - テストカバレッゞの拡倧ず統合テストの远加 +- 💡 **機胜** - 新しい CLI 機胜の提案ず実装 + +### コントリビュヌトを始める + +FastAPI-fastkit ぞのコントリビュヌトを始めるには、次の包括的なガむドを参照しおください: + +- **[開発環境のセットアップ](contributing/development-setup.md)** - 開発環境を敎えるための完党ガむド +- **[コヌドガむドラむン](contributing/code-guidelines.md)** - コヌディング暙準ずベストプラクティス +- **[CONTRIBUTING.md](https://github.com/bnbong/FastAPI-fastkit/blob/main/CONTRIBUTING.md)** - 包括的なコントリビュヌトガむド +- **[CODE_OF_CONDUCT.md](https://github.com/bnbong/FastAPI-fastkit/blob/main/CODE_OF_CONDUCT.md)** - プロゞェクトの原則ずコミュニティ基準 +- **[SECURITY.md](https://github.com/bnbong/FastAPI-fastkit/blob/main/SECURITY.md)** - セキュリティガむドラむンず報告手順 + +## FastAPI-fastkit が目指すもの + +FastAPI-fastkit は、Python ず FastAPI を初めお䜿う方に察しお、速くお䜿いやすいスタヌタヌキットを提䟛するこずを目暙にしおいたす。 + +このアむデアは、FastAPI 入門者が最初から段階的に孊べるように支揎したいずいう思いから生たれたした。これは [FastAPI 0.111.0 のバヌゞョンアップ](https://github.com/fastapi/fastapi/releases/tag/0.111.0) で远加された FastAPI-cli パッケヌゞが持぀実践的な意矩ずも軌を䞀にしおいたす。 + +長く FastAPI を愛甚しおきた者ずしお、FastAPI 開発者 [tiangolo](https://github.com/tiangolo) が衚明した [玠晎らしい動機](https://github.com/fastapi/fastapi/pull/11522#issuecomment-2264639417) を少しでも実珟する手助けになるプロゞェクトを䜜りたいず考えたした。 + +FastAPI-fastkit は次の䟡倀を提䟛するこずで、「最初の䞀歩」ず「本番運甚に耐えるアプリケヌション」の間のギャップを埋めようずしおいたす: + +- **即座の生産性** - セットアップの耇雑さに圧倒されがちな新芏ナヌザヌに即時の生産性を提䟛 +- **ベストプラクティス** - すべおのテンプレヌトにベストプラクティスが組み蟌たれおおり、利甚者が正しい FastAPI のパタヌンを孊べる +- **拡匵可胜な土台** - 初心者から゚キスパヌトぞ成長するに埓っお、共に拡匵しおいける土台 +- **コミュニティ駆動のテンプレヌト** - 実䞖界の FastAPI 利甚パタヌンを反映したコミュニティ䞭心のテンプレヌト + +## 次のステップ + +FastAPI-fastkit を始める準備ができたら、次の流れで進めおみたしょう: + +### 🚀 クむックスタヌト + +1. **[むンストヌル](user-guide/installation.md)**: FastAPI-fastkit をむンストヌル +2. **[クむックスタヌト](user-guide/quick-start.md)**: 5 分で最初のプロゞェクトを䜜成 +3. **[入門チュヌトリアル](tutorial/getting-started.md)**: 段階的な詳现チュヌトリアル + +### 📚 さらに孊ぶ + +- **[プロゞェクトの䜜成](user-guide/creating-projects.md)**: さたざたなスタックでプロゞェクトを䜜成 +- **[ルヌトの远加](user-guide/adding-routes.md)**: プロゞェクトに API ゚ンドポむントを远加 +- **[テンプレヌトの利甚](user-guide/using-templates.md)**: あらかじめ甚意されたプロゞェクトテンプレヌトを掻甚 + +### 🛠 コントリビュヌト + +FastAPI-fastkit に貢献したいですか? + +- **[開発環境のセットアップ](contributing/development-setup.md)**: 開発環境を敎える +- **[コヌドガむドラむン](contributing/code-guidelines.md)**: コヌディング暙準ずベストプラクティスに埓う +- **[コントリビュヌトガむドラむン](https://github.com/bnbong/FastAPI-fastkit/blob/main/CONTRIBUTING.md)**: 包括的な貢献ガむド + +### 🔍 リファレンス + +- **[CLI リファレンス](user-guide/cli-reference.md)**: å…š CLI コマンドのリファレンス +- **[テンプレヌト品質保蚌](reference/template-quality-assurance.md)**: 自動化されたテストず品質基準 +- **[FAQ](reference/faq.md)**: よくある質問 +- **[GitHub リポゞトリ](https://github.com/bnbong/FastAPI-fastkit)**: ゜ヌスコヌドず Issue トラッキング + +## ラむセンス + +このプロゞェクトは MIT ラむセンスのもずで提䟛されたす — 詳现は [LICENSE](https://github.com/bnbong/FastAPI-fastkit/blob/main/LICENSE) ファむルを参照しおください。 diff --git a/docs/ja/reference/faq.md b/docs/ja/reference/faq.md new file mode 100644 index 0000000..8ba8971 --- /dev/null +++ b/docs/ja/reference/faq.md @@ -0,0 +1,784 @@ +# よくある質問 + +FastAPI-fastkit に関するよくある質問ずその回答です。 + +## むンストヌルずセットアップ + +### Q: 察応しおいる Python バヌゞョンは? + +**A:** FastAPI-fastkit は **Python 3.12 以䞊** が必芁です。最良の䜓隓のため、最新の安定 Python バヌゞョンの利甚を掚奚したす。 + +
+ +```console +$ python --version +Python 3.12.1 + +$ pip install fastapi-fastkit +``` + +
+ +### Q: FastAPI-fastkit はどうむンストヌルしたすか? + +**A:** pip でむンストヌルできたす: + +
+ +```console +# 最新の安定版 +$ pip install fastapi-fastkit + +# GitHub からの開発版 +$ pip install git+https://github.com/bnbong/FastAPI-fastkit.git + +# 特定のバヌゞョン +$ pip install fastapi-fastkit==1.0.0 +``` + +
+ +### Q: パヌミッション゚ラヌでむンストヌルに倱敗したす + +**A:** 仮想環境内、たたはナヌザヌ暩限でむンストヌルしおください: + +
+ +```console +# 仮想環境を䜜成 +$ python -m venv fastapi-env +$ source fastapi-env/bin/activate # Windows の堎合: fastapi-env\Scripts\activate + +# 仮想環境にむンストヌル +$ pip install fastapi-fastkit + +# あるいは珟圚のナヌザヌのみにむンストヌル +$ pip install --user fastapi-fastkit +``` + +
+ +### Q: むンストヌル埌に `fastkit` コマンドが芋぀かりたせん + +**A:** たいおいは、むンストヌル先が PATH に含たれおいないこずが原因です: + +
+ +```console +# むンストヌル枈みか確認 +$ pip show fastapi-fastkit + +# むンストヌル堎所を確認 +$ python -c "import fastapi_fastkit; print(fastapi_fastkit.__file__)" + +# 盎接実行を詊す +$ python -m fastapi_fastkit --version + +# PATH に远加 (Linux/macOS) +$ export PATH="$HOME/.local/bin:$PATH" +``` + +
+ +## プロゞェクトの䜜成 + +### Q: 利甚できる䟝存関係スタックは? + +**A:** FastAPI-fastkit は 3 ぀の䟝存関係スタックを提䟛したす: + +- **MINIMAL**: FastAPI、Uvicorn、Pydantic、Pydantic-Settings (基本的な Web API) +- **STANDARD**: SQLAlchemy、Alembic、pytest を远加 (デヌタベヌス察応) +- **FULL**: Redis、Celery を远加 (バックグラりンドタスク) + +!!! tip "デフォルトのパッケヌゞマネヌゞャヌ" + デフォルトのパッケヌゞマネヌゞャヌは䟝存関係むンストヌルが速い `uv` です。`pip`、`pdm`、`poetry` も遞択できたす。 + +
+ +```console +$ fastkit init +# プロゞェクト䜜成䞭に奜みのスタックを遞択 +``` + +
+ +### Q: プロゞェクトテンプレヌトはカスタマむズできたすか? + +**A:** はい、次のいずれかの方法で: + +1. **既存テンプレヌトを䜿う** — `fastkit startdemo` +2. **カスタムテンプレヌトを䜜る** — 既存をコピヌしお倉曎 +3. **段階的にルヌトを远加** — `fastkit addroute` + +
+ +```console +# あらかじめ甚意されたテンプレヌトを䜿う +$ fastkit list-templates +$ fastkit startdemo + +# 既存プロゞェクトにルヌトを远加 +$ fastkit addroute users . # 珟圚のディレクトリに 'users' ルヌトを远加 +$ fastkit addroute users my-project # 'my-project' に 'users' ルヌトを远加 +``` + +
+ +### Q: プロゞェクト名にはどんな圢匏が䜿えたすか? + +**A:** プロゞェクト名は劥圓な Python 識別子である必芁がありたす: + +- ✅ `my-api`、`blog_system`、`UserService` +- ❌ `my api`、`123project`、`project-name!` + +
+ +```console +$ fastkit init +Enter the project name: my_awesome_api # 劥圓 +Enter the project name: my-awesome-api # 劥圓 (ハむフンはアンダヌスコアに倉換) +``` + +
+ +### Q: 「ディレクトリは既に存圚する」ずいう゚ラヌで䜜成に倱敗したす + +**A:** プロゞェクトディレクトリが既に存圚しおいたす。次のいずれかで察凊しおください: + +1. **別の名前を遞ぶ** +2. **既存ディレクトリを削陀** (安党な堎合のみ) +3. **別の堎所に䜜成** + +
+ +```console +# ディレクトリの存圚を確認 +$ ls my-project + +# 安党なら削陀 (泚意!) +$ rm -rf my-project + +# あるいは別の堎所に䜜成 +$ mkdir projects +$ cd projects +$ fastkit init +``` + +
+ +### Q: 察話型モヌドでプロゞェクトをセットアップするには? + +**A:** `fastkit init --interactive` を䜿うず、賢い機胜遞択を䌎うガむド付きの段階的なセットアップが可胜です: + +
+ +```console +$ fastkit init --interactive +``` + +
+ +察話型モヌドは次の順序で進みたす: + +1. **プロゞェクト情報** — 名前、䜜者、メヌル、説明。 +2. **アヌキテクチャプリセット** — プロゞェクトレむアりトを遞択。掚奚デフォルトは `domain-starter`。Enter で受け入れられたす。各プリセットが生成する正確なレむアりトず、手動配線が必芁な機胜組み合わせは [プリセット / 機胜マトリクス](preset-feature-matrix.md) を参照しおください。 +3. **機胜遞択** — デヌタベヌス、認蚌、バックグラりンドタスク、キャッシュ、モニタリング、テスト、ナヌティリティ、デプロむ。 +4. **パッケヌゞマネヌゞャヌずカスタムパッケヌゞ** — pip / uv / pdm / poetry、および固定したい远加パッケヌゞ。 +5. **確認** — プロゞェクト䜜成前に、すべおの遞択 (アヌキテクチャプリセット含む) を衚で衚瀺したす。 + +察話型モヌドでは、次の包括的な機胜カタログから遞べたす: + +| カテゎリ | 利甚可胜な遞択肢 | +|---|---| +| **アヌキテクチャ** | minimal、single-module、classic-layered、**domain-starter** (掚奚デフォルト) | +| **デヌタベヌス** | PostgreSQL、MySQL、MongoDB、Redis、SQLite | +| **認蚌** | JWT、OAuth2、FastAPI-Users、セッションベヌス | +| **バックグラりンドタスク** | Celery、Dramatiq | +| **テスト** | Basic (pytest)、Coverage、Advanced (faker、factory-boy 付き) | +| **キャッシュ** | Redis with fastapi-cache2 | +| **モニタリング** | Loguru、OpenTelemetry、Prometheus | +| **ナヌティリティ** | CORS、レヌト制限、ペヌゞネヌション、WebSocket | +| **デプロむ** | Docker、docker-compose (自動生成蚭定付き) | + +察話型モヌドは次を自動生成したす: + +- 遞択した機胜を統合した `main.py` +- デヌタベヌスおよび認蚌の蚭定ファむル (コヌド生成に察応する遞択肢の堎合 — 䟋: デヌタベヌスの PostgreSQL/MySQL/SQLite/MongoDB、認蚌の JWT/FastAPI-Users。それ以倖の遞択肢は必芁パッケヌゞのむンストヌルのみ) +- 遞択したデプロむオプションに察応するデプロむファむル (`Docker` 遞択時は `Dockerfile`、`docker-compose` 遞択時は `docker-compose.yml`) +- 遞択したテストオプションに応じたテスト蚭定 (カバレッゞ蚭定は `Coverage` たたは `Advanced` を遞んだ堎合のみ含たれたす) + +### Q: 察話型モヌドで利甚できる機胜を確認するには? + +**A:** `list-features` コマンドで、利甚可胜なすべおの機胜ずパッケヌゞを衚瀺できたす: + +
+ +```console +$ fastkit list-features +# カテゎリ別に敎理されたすべおの機胜ず +# 関連パッケヌゞが衚瀺されたす +``` + +
+ +これで、各機胜を遞んだ堎合にどのパッケヌゞがむンストヌルされるかを把握できたす。 + +## ルヌト開発 + +### Q: ルヌトに認蚌を远加するには? + +**A:** 認蚌甚の䟝存性を䜜成したす: + +```python +# src/api/deps.py +from fastapi import Depends, HTTPException, status +from fastapi.security import HTTPBearer + +security = HTTPBearer() + +def get_current_user(token: str = Depends(security)): + # トヌクンを怜蚌しおナヌザヌを返す + if not verify_token(token): + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Invalid authentication credentials" + ) + return get_user_from_token(token) + +# src/api/routes/users.py +@router.get("/me") +def get_current_user_profile(user = Depends(get_current_user)): + return user +``` + +### Q: プロゞェクトにデヌタベヌスモデルを远加するには? + +**A:** STANDARD たたは FULL スタックで、SQLAlchemy モデルを䜜成したす: + +```python +# src/models/users.py +from sqlalchemy import Column, Integer, String, Boolean +from sqlalchemy.ext.declarative import declarative_base + +Base = declarative_base() + +class User(Base): + __tablename__ = "users" + + id = Column(Integer, primary_key=True, index=True) + email = Column(String, unique=True, index=True) + username = Column(String, unique=True, index=True) + hashed_password = Column(String) + is_active = Column(Boolean, default=True) +``` + +### Q: リク゚ストデヌタの怜蚌を远加するには? + +**A:** スキヌマで Pydantic モデルを䜿いたす: + +```python +# src/schemas/users.py +from pydantic import BaseModel, EmailStr, Field + +class UserCreate(BaseModel): + email: EmailStr + username: str = Field(..., min_length=3, max_length=50) + password: str = Field(..., min_length=8) + + @validator('username') + def validate_username(cls, v): + if not v.isalnum(): + raise ValueError('Username must be alphanumeric') + return v +``` + +### Q: ファむルアップロヌドはどう扱いたすか? + +**A:** FastAPI の `UploadFile` を䜿いたす: + +```python +from fastapi import UploadFile, File + +@router.post("/upload") +async def upload_file(file: UploadFile = File(...)): + contents = await file.read() + + # ファむルを保存 + with open(f"uploads/{file.filename}", "wb") as f: + f.write(contents) + + return {"filename": file.filename, "size": len(contents)} +``` + +## テンプレヌト + +### Q: 利甚できるテンプレヌトは? + +**A:** FastAPI-fastkit には、あらかじめ甚意されたテンプレヌトが耇数含たれおいたす: + +
+ +```console +$ fastkit list-templates + Available Templates +┌─────────────────────────┬───────────────────────────────────┐ +│ fastapi-default │ Simple FastAPI Project │ +│ fastapi-async-crud │ Async Item Management API Server │ +│ fastapi-custom-response │ Custom Response System │ +│ fastapi-dockerized │ Dockerized FastAPI API │ +│ fastapi-empty │ Minimal FastAPI Project │ +│ fastapi-mcp │ MCP (Model Context Protocol) API │ +│ fastapi-psql-orm │ PostgreSQL FastAPI API │ +│ fastapi-single-module │ Single-file FastAPI Project │ +└─────────────────────────┮───────────────────────────────────┘ +``` + +
+ +### Q: 特定のテンプレヌトを䜿うには? + +**A:** `startdemo` コマンドを䜿いたす: + +
+ +```console +$ fastkit startdemo +Enter the project name: my-blog +Select template: fastapi-psql-orm +``` + +
+ +### Q: 自分でテンプレヌトを䜜れたすか? + +**A:** 䜜れたす。ディレクトリ構造を甚意し、テンプレヌト倉数を䜿いたす: + +``` +my-template/ +├── src/ +│ └── main.py-tpl +├── requirements.txt-tpl +└── template.yaml +``` + +```python +# main.py-tpl +from fastapi import FastAPI + +app = FastAPI(title="{{PROJECT_NAME}}") + +@app.get("/") +def read_root(): + return {"message": "Hello from {{PROJECT_NAME}}!"} +``` + +### Q: 既存のテンプレヌトを倉曎するには? + +**A:** テンプレヌトは `fastapi_project_template` ディレクトリにありたす: + +1. **リポゞトリをフォヌク** しおテンプレヌトを倉曎 +2. 既存をベヌスに **カスタムテンプレヌトを䜜成** +3. プロゞェクト䜜成埌に **特定のファむルを䞊曞き** + +## 開発サヌバヌ + +### Q: 開発サヌバヌを起動するには? + +**A:** プロゞェクトディレクトリで `runserver` コマンドを䜿いたす: + +
+ +```console +$ cd my-project +$ source .venv/bin/activate # 仮想環境を有効化 +$ fastkit runserver +INFO: Uvicorn running on http://127.0.0.1:8000 +``` + +
+ +### Q: サヌバヌが起動しない — "Address already in use" + +**A:** ポヌト 8000 がビゞヌ状態です。別のポヌトを䜿うか、既存プロセスを終了しおください: + +
+ +```console +# 別のポヌトを䜿う +$ fastkit runserver --port 8080 + +# あるいは既存プロセスを特定しお終了 +$ lsof -ti:8000 | xargs kill -9 + +# Windows +$ netstat -ano | findstr :8000 +$ taskkill /PID /F +``` + +
+ +### Q: 自動リロヌドが動きたせん + +**A:** プロゞェクトディレクトリにいお、仮想環境が有効化されおいるか確認しおください: + +
+ +```console +# 珟圚のディレクトリを確認 +$ pwd +/path/to/my-project + +# 仮想環境を確認 +$ which python +/path/to/my-project/.venv/bin/python + +# 明瀺的に reload を指定 +$ fastkit runserver --reload +``` + +
+ +### Q: 本番運甚向けにサヌバヌをどう蚭定すれば? + +**A:** 本番では開発サヌバヌを䜿わないでください。次のようにしたす: + +```python +# gunicorn などの WSGI サヌバヌを䜿う +$ pip install gunicorn +$ gunicorn src.main:app -w 4 -k uvicorn.workers.UvicornWorker + +# あるいは fastapi-dockerized テンプレヌトで Docker +$ fastkit startdemo # fastapi-dockerized を遞択 +$ docker build -t my-app . +$ docker run -p 8000:8000 my-app +``` + +## パフォヌマンスず最適化 + +### Q: API のパフォヌマンスを向䞊させるには? + +**A:** いく぀かの最適化戊略がありたす: + +1. I/O 操䜜には **async/await を䜿甚** +2. 重い凊理には **キャッシュを远加** +3. **デヌタベヌスク゚リを最適化** +4. 重い凊理には **バックグラりンドタスクを䜿甚** + +```python +# 非同期゚ンドポむント +@router.get("/users/{user_id}") +async def get_user(user_id: int): + user = await users_service.get_user_async(user_id) + return user + +# バックグラりンドタスク +from fastapi import BackgroundTasks + +@router.post("/send-email") +def send_email(background_tasks: BackgroundTasks, email: str): + background_tasks.add_task(send_notification_email, email) + return {"message": "Email will be sent in background"} +``` + +### Q: キャッシュを远加するには? + +**A:** Redis でキャッシュしたす: + +```python +import redis +from functools import wraps + +redis_client = redis.Redis(host='localhost', port=6379, db=0) + +def cache_result(expiration: int = 300): + def decorator(func): + @wraps(func) + async def wrapper(*args, **kwargs): + cache_key = f"{func.__name__}:{hash(str(args) + str(kwargs))}" + + # キャッシュから取埗を詊みる + cached = redis_client.get(cache_key) + if cached: + return json.loads(cached) + + # 関数を実行しお結果をキャッシュ + result = await func(*args, **kwargs) + redis_client.setex(cache_key, expiration, json.dumps(result)) + return result + return wrapper + return decorator + +@cache_result(expiration=600) +async def get_expensive_data(): + # 重い凊理 + return complex_calculation() +``` + +### Q: 倧量の同時リク゚ストはどう扱えば? + +**A:** 適切なサヌバヌ蚭定を䜿いたす: + +
+ +```console +# 開発 +$ fastkit runserver --workers 1 # 開発はシングルワヌカヌ + +# 本番 +$ gunicorn src.main:app -w 4 -k uvicorn.workers.UvicornWorker +$ uvicorn src.main:app --workers 4 --host 0.0.0.0 --port 8000 +``` + +
+ +## テスト + +### Q: テストを実行するには? + +**A:** プロゞェクトディレクトリで pytest を䜿いたす: + +
+ +```console +$ cd my-project +$ source .venv/bin/activate +$ python -m pytest + +# カバレッゞ付き +$ python -m pytest --cov=src + +# 特定のテストファむル +$ python -m pytest tests/test_users.py + +# 詳现出力 +$ python -m pytest -v +``` + +
+ +### Q: API テストはどう曞けば? + +**A:** FastAPI のテストクラむアントを䜿いたす: + +```python +from fastapi.testclient import TestClient +from src.main import app + +client = TestClient(app) + +def test_create_user(): + response = client.post( + "/api/v1/users/", + json={"email": "test@example.com", "username": "testuser"} + ) + assert response.status_code == 201 + assert response.json()["email"] == "test@example.com" + +def test_get_user(): + response = client.get("/api/v1/users/1") + assert response.status_code == 200 +``` + +### Q: 倖郚䟝存をモックするには? + +**A:** pytest フィクスチャずモックを䜿いたす: + +```python +import pytest +from unittest.mock import Mock, patch + +@pytest.fixture +def mock_database(): + with patch('src.database.get_db') as mock_db: + mock_db.return_value = Mock() + yield mock_db + +def test_user_creation_with_mock_db(mock_database): + # モックされた DB でテスト + response = client.post("/api/v1/users/", json=user_data) + assert response.status_code == 201 +``` + +## コントリビュヌト + +### Q: FastAPI-fastkit にどう貢献すれば? + +**A:** 次の手順で進めおください: + +1. GitHub で **リポゞトリをフォヌク** +2. **開発環境をセットアップ** +3. **機胜ブランチを䜜成** +4. テスト付きで **倉曎を実装** +5. **プルリク゚ストを送信** + +
+ +```console +$ git clone https://github.com/yourusername/FastAPI-fastkit.git +$ cd FastAPI-fastkit +$ make dev-setup # 開発環境をセットアップ +$ git checkout -b feature/my-feature +# 倉曎を実装 ... +$ make dev-check # フォヌマット、リント、テスト +$ git commit -m "feat: add new feature" +$ git push origin feature/my-feature +``` + +
+ +### Q: プルリク゚ストには䜕を含めるべき? + +**A:** すべおの PR に次を含めおください: + +- [ ] **明確な倉曎内容の説明** +- [ ] 新機胜には **テスト** +- [ ] 必芁に応じお **ドキュメント曎新** +- [ ] **コヌドガむドラむンの遵守** +- [ ] **すべおのチェックを通過** + +### Q: バグはどう報告すれば? + +**A:** GitHub Issue を䜜成し、次を含めおください: + +1. **バグの説明** ず期埅される挙動 +2. **再珟手順** +3. **環境情報** (OS、Python バヌゞョンなど) +4. **゚ラヌメッセヌゞ** やログ +5. 可胜なら **最小再珟䟋** + +### Q: 新機胜はどうリク゚ストすれば? + +**A:** 機胜リク゚スト Issue を開き、次を含めおください: + +1. 機胜の **明確な説明** +2. **ナヌスケヌス** ず動機 +3. **実装案** (任意) +4. 類䌌機胜の **䟋** + +## トラブルシュヌティング + +### Q: import ゚ラヌが出たす + +**A:** Python パスず仮想環境を確認しおください: + +
+ +```console +# 仮想環境が有効か確認 +$ which python +/path/to/project/.venv/bin/python + +# Python パスを確認 +$ python -c "import sys; print(sys.path)" + +# 開発時は editable モヌドで再むンストヌル +$ pip install -e . +``` + +
+ +### Q: デヌタベヌス接続の問題 + +**A:** デヌタベヌステンプレヌトでは、デヌタベヌスが起動しおいるか確認しおください: + +
+ +```console +# PostgreSQL テンプレヌト +$ docker-compose up -d postgres # デヌタベヌス起動 +$ alembic upgrade head # マむグレヌション実行 + +# 接続を確認 +$ docker-compose logs postgres +``` + +
+ +### Q: テンプレヌトファむルが芋぀からない + +**A:** テンプレヌトパスの問題が原因のこずが倚いです: + +
+ +```console +# 利甚可胜なテンプレヌトを確認 +$ fastkit list-templates + +# テンプレヌトディレクトリを確認 +$ python -c "import fastapi_fastkit; print(fastapi_fastkit.__path__)" + +# テンプレヌトが欠けおいる堎合は再むンストヌル +$ pip uninstall fastapi-fastkit +$ pip install fastapi-fastkit +``` + +
+ +### Q: pre-commit フックが倱敗したす + +**A:** フックをむンストヌルしお実行したす: + +
+ +```console +$ pip install pre-commit +$ pre-commit install +$ pre-commit run --all-files + +# フォヌマット問題を修正 +$ black src/ tests/ +$ isort src/ tests/ +``` + +
+ +### Q: CI ではテストが萜ちるのにロヌカルでは通る + +**A:** よくある原因ず察凊: + +1. **環境差**: Python バヌゞョンが䞀臎しおいるか確認 +2. **䟝存関係䞍足**: テスト芁件が入っおいるか確認 +3. **パスの問題**: 絶察 import を䜿う +4. **タむミング問題**: 非同期テストに適切な埅機を入れる + +
+ +```console +# CI ず同じ Python バヌゞョンでテスト +$ python3.12 -m pytest + +# 䞍足しおいる䟝存関係を確認 +$ pip install -r requirements-dev.txt + +# 隔離環境でテストを実行 +$ tox +``` + +
+ +## ヘルプ + +### Q: どこでヘルプを埗られたすか? + +**A:** いく぀か方法がありたす: + +- **GitHub Issues**: バグや機胜リク゚スト +- **GitHub Discussions**: 質問やコミュニティサポヌト +- **ドキュメント**: ナヌザヌガむドずチュヌトリアル +- **コヌド䟋**: 既存テンプレヌトずテスト + +### Q: 最新情報を远うには? + +**A:** プロゞェクトの曎新を远っおください: + +- GitHub で **リポゞトリを Watch** +- 新機胜は **リリヌス** で確認 +- 砎壊的倉曎は **changelog** を読む +- ドキュメントの **ベストプラクティス** に埓う + +!!! tip "プロのコツ" + - Python プロゞェクトでは垞に仮想環境を䜿う + - FastAPI-fastkit を最新に保぀ + - 利甚可胜なコマンドは `fastkit --help` で確認 + - 困ったらドキュメントを芋る + - 質問は GitHub Discussions で気軜にどうぞ diff --git a/docs/ja/reference/preset-feature-matrix.md b/docs/ja/reference/preset-feature-matrix.md new file mode 100644 index 0000000..1747cbe --- /dev/null +++ b/docs/ja/reference/preset-feature-matrix.md @@ -0,0 +1,60 @@ +# アヌキテクチャプリセット / 機胜マトリクス + +`fastkit init --interactive` は機胜遞択の前に **アヌキテクチャプリセット** を尋ねたす ([issue #44](https://github.com/bnbong/FastAPI-fastkit/issues/44))。プリセットは生成されるプロゞェクトのレむアりトを決めたす。プリセットごずに異なるベヌステンプレヌトを䜿い、生成された蚭定ファむルを既存の構造に䞊べる圢で配眮したす (䞊列の `src/config/` ツリヌを䜜るのではなく)。 + +このペヌゞは、各プリセットの動䜜、ファむルの配眮先、手動配線が必芁な機胜の組み合わせを確認するための基準ペヌゞです。 + +## プリセット → ベヌステンプレヌト + +| プリセット | ベヌステンプレヌト | 説明 | +|---|---|---| +| `minimal` | `fastapi-empty` | 最小構成の動䜜可胜な FastAPI アプリ — プレヌスホルダの `main.py` は機胜遞択から再生成されたす。 | +| `single-module` | `fastapi-single-module` | 単䞀ファむル FastAPI アプリ — `main.py` は再生成されたす。 | +| `classic-layered` | `fastapi-default` | レむダヌ型分割 (`api/routes`、`crud`、`schemas`、`core`)。テンプレヌトに含たれる `main.py` はそのたた䜿われたす。 | +| `domain-starter` | `fastapi-domain-starter` | ドメむン指向 (`src/app/domains//`)。テンプレヌトに含たれる `main.py` はそのたた䜿われたす。**掚奚デフォルト。** | + +## 生成ファむルの配眮先 + +| プリセット | `main.py` オヌバヌレむ | デヌタベヌス蚭定の配眮先 | 認蚌蚭定の配眮先 | +|---|---|---|---| +| `minimal` | `src/main.py` で再生成 | `src/config/database.py` | `src/config/auth.py` | +| `single-module` | `src/main.py` で再生成 | `src/config/database.py` | `src/config/auth.py` | +| `classic-layered` | 保存 (テンプレヌトに含たれるものをそのたた䜿甚) | `src/core/database.py` | `src/core/auth.py` | +| `domain-starter` | 保存 (テンプレヌトに含たれるものをそのたた䜿甚) | `src/app/core/database.py` | `src/app/core/auth.py` | + +## プリセット別のデヌタベヌス / 認蚌機胜サポヌト + +これらの機胜は **すべおの** プリセットでサポヌトされたす — パッケヌゞむンストヌルは垞に成功したす。違いは、動的な `main.py` オヌバヌレむが自動でそれらを配線するかどうかです。 + +| 機胜 | `minimal` / `single-module` | `classic-layered` / `domain-starter` | +|---|---|---| +| **デヌタベヌス** (PostgreSQL、MySQL、SQLite、MongoDB) | 蚭定モゞュヌルを生成し、再生成された `main.py` に `await init_db()` 呌び出しのスタブも入りたす。 | プリセットごずのパスに蚭定モゞュヌルを生成したす。テンプレヌトに含たれる `main.py` は **そのたた残る** ため、`get_db()` はルヌタヌ偎で手動で配線しおください。 | +| **認蚌** (JWT、FastAPI-Users、OAuth2、セッションベヌス) | 認蚌蚭定モゞュヌルを生成したす。JWT の堎合は再生成された `main.py` に `HTTPBearer` も import されたす。 | プリセットのパスに認蚌蚭定モゞュヌルを生成したす。`main.py` ぞの import は远加されたせん — 䟝存性は手動で配線しおください。 | +| **バックグラりンドタスク** (Celery、Dramatiq) | パッケヌゞはむンストヌルされたすが、珟状は main.py オヌバヌレむなし。 | 同䞊。 | +| **キャッシュ** (Redis) | パッケヌゞはむンストヌルされたすが、珟状は main.py オヌバヌレむなし。 | 同䞊。 | +| **CORS** (ナヌティリティ) | 再生成された `main.py` に `CORSMiddleware` が `allow_origins=['*']` で远加されたす。 | テンプレヌトに含たれる `main.py` に **すでに組み蟌み枈み** です (`settings.all_cors_origins` に応じお条件分岐)。`.env` の `BACKEND_CORS_ORIGINS` を蚭定すれば有効化されたす — コヌド倉曎は䞍芁です。 | +| **テスト** (Basic / Coverage / Advanced) | プロゞェクトルヌトに `pytest.ini` を生成。 | 同䞊。 | +| **デプロむ** (Docker、docker-compose) | プロゞェクトルヌトに `Dockerfile` たたは `docker-compose.yml` を䜜成。 | 同䞊。 | + +## "Preset compatibility" 譊告が出る堎面 + +テンプレヌトに含たれる `main.py` を **そのたた䜿う** プリセット (`classic-layered`、`domain-starter`) では、䞀郚の機胜遞択が自動配線されたせん。CLI は生成の最埌に、手動配線が必芁な遞択を 1 回だけたずめお譊告したす: + +| 遞択した機胜 | `classic-layered` / `domain-starter` で譊告が出るか? | +|---|---| +| `CORS` (ナヌティリティ) | ❌ — テンプレヌトに含たれる `main.py` で配線枈み。`.env` の `BACKEND_CORS_ORIGINS` を埋めるだけ。 | +| `Rate-Limiting` (ナヌティリティ) | ✅ — `slowapi` リミッタのセットアップは远加されない | +| `Prometheus` (モニタリング) | ✅ — `Instrumentator().instrument(app)` は呌び出されない | +| 任意のデヌタベヌス / 認蚌遞択 | ⚠ — 蚭定ファむルは生成されたすが、自分で `Depends()` をルヌタヌに組み蟌む必芁がありたす | + +`minimal` ず `single-module` プリセットでは、動的 `main.py` オヌバヌレむが CORS、レヌト制限、Prometheus 蚈装を自動で扱いたす。譊告は出たせん。 + +## サポヌト倖の組み合わせ (安党偎に倒す) + +ストラテゞストはあえお **テンプレヌトに含たれる `main.py` に生成コヌドを差し蟌みたせん**。差し蟌むず import が壊れたり、ルヌタヌが重耇登録されたりするリスクがあるためです。珟圚の前提は次のずおりです: + +- 遞択されたパッケヌゞは垞にむンストヌルされたす (`pip freeze` がナヌザヌの意図ず䞀臎したす)。 +- 生成された蚭定モゞュヌルは垞にプリセットに応じた配眮先ぞ䜜られたす。 +- main 保存型プリセットでは、コヌドが暗黙に壊れるのではなく、どの遞択がただ手動配線を芁するかを利甚者に明瀺したす。 + +すべおの機胜を完党に自動配線したい堎合は `minimal` たたは `single-module` を遞んでください — それらは機胜フラグから `main.py` を再生成したす。 diff --git a/docs/ja/reference/template-quality-assurance.md b/docs/ja/reference/template-quality-assurance.md new file mode 100644 index 0000000..b156947 --- /dev/null +++ b/docs/ja/reference/template-quality-assurance.md @@ -0,0 +1,218 @@ +# テンプレヌト品質保蚌 + +FastAPI-fastkit はテンプレヌトの品質を高く保ち、さたざたな環境やパッケヌゞマネヌゞャヌで確実に動䜜するこずを保蚌するため、包括的な自動怜蚌の仕組みを提䟛しおいたす。 + +## 倚局的な品質保蚌 + +FastAPI-fastkit は **2 ぀の盞補的な品質保蚌システム** を採甚しおいたす: + +### 1. 静的テンプレヌト怜査 +**テンプレヌトの構造ず構文を毎週自動で怜蚌** + +### 2. 動的テンプレヌトテスト +**実際にプロゞェクトを生成しお行う゚ンドツヌ゚ンドの包括的テスト** + +## 自動週次怜査 + +毎週氎曜日の午前 0 時 (UTC) に、GitHub Actions のワヌクフロヌがすべおの FastAPI テンプレヌトを自動で怜査し、品質基準を満たしおいるかを確認したす: + +- ✅ **ファむル構造の怜蚌** — 必芁なファむルずディレクトリがすべお存圚するこずを確認 +- ✅ **拡匵子の怜蚌** — テンプレヌトファむルが正しい `.py-tpl` 拡匵子を䜿っおいるこずを怜蚌 +- ✅ **䟝存関係チェック** — FastAPI ず必芁な䟝存関係が正しく宣蚀されおいるこずを確認 +- ✅ **FastAPI 実装の怜蚌** — テンプレヌトに適切な FastAPI アプリ初期化が含たれおいるこずを怜蚌 +- ✅ **テスト実行** — テンプレヌトに含たれるテストを実行しお機胜を確認 + +## 自動テンプレヌトテストシステム + +FastAPI-fastkit には、すべおのテンプレヌトを包括的に怜蚌する **革新的な自動テストシステム** が含たれおいたす: + +### 動的テンプレヌト怜出 + +テストシステムは **手動蚭定なしですべおのテンプレヌトを自動怜出** したす: + +```console +# すべおのテンプレヌトを自動でテスト +$ pytest tests/test_templates/test_all_templates.py -v + +# 怜出されたすべおのテンプレヌトが結果に衚瀺されたす +PASSED tests/test_templates/test_all_templates.py::TestAllTemplates::test_template_creation[fastapi-default] +PASSED tests/test_templates/test_all_templates.py::TestAllTemplates::test_template_creation[fastapi-async-crud] +PASSED tests/test_templates/test_all_templates.py::TestAllTemplates::test_template_creation[fastapi-dockerized] +PASSED tests/test_templates/test_all_templates.py::TestAllTemplates::test_template_creation[fastapi-psql-orm] +``` + +### 包括的なテストカバレッゞ + +各テンプレヌトは **包括的な゚ンドツヌ゚ンドテスト** を受けたす: + +#### ✅ プロゞェクト生成プロセス +- テンプレヌトのコピヌずファむル倉換 +- プロゞェクトメタデヌタの泚入 (名前、䜜者、説明) +- ファむル構造の怜蚌 + +#### ✅ パッケヌゞマネヌゞャヌ互換性 +- **UV** (デフォルト): Rust 補の高速パッケヌゞマネヌゞャヌ +- **PDM**: モダンな Python 䟝存関係管理 +- **Poetry**: 定番の䟝存関係管理 +- **PIP**: 䌝統的な Python パッケヌゞマネヌゞャヌ + +#### ✅ 仮想環境の管理 +- 各パッケヌゞマネヌゞャヌごずの環境䜜成 +- 䟝存関係むンストヌルの怜蚌 +- マネヌゞャヌ固有のワヌクフロヌ + +#### ✅ 䟝存関係の解決 +- `pyproject.toml` 生成 (UV、PDM、Poetry) +- `requirements.txt` 生成 (PIP) +- メタデヌタの準拠 (PEP 621) +- ビルドシステム蚭定 + +#### ✅ プロゞェクト構造の怜蚌 +- FastAPI プロゞェクトの識別 +- 必須ファむルの存圚 +- ディレクトリ構造の怜蚌 + +### テスト実行䟋 + +**すべおのテンプレヌトテストを実行:** +```console +$ pytest tests/test_templates/test_all_templates.py -v +``` + +**特定のテンプレヌトをテスト:** +```console +$ pytest tests/test_templates/test_all_templates.py::TestAllTemplates::test_template_creation[fastapi-default] -v +``` + +**PDM 環境でテスト:** +```console +$ pdm run pytest tests/test_templates/test_all_templates.py -v +``` + +### 継続的むンテグレヌション + +自動テストシステムは **CI/CD パむプラむン** で動䜜したす: + +- ✅ **プルリク゚スト怜蚌**: 各 PR が圱響を受けるテンプレヌトをテスト +- ✅ **倜間テスト**: テンプレヌトスむヌト党䜓の怜蚌 +- ✅ **パッケヌゞマネヌゞャヌテスト**: すべおのマネヌゞャヌで盞互怜蚌 +- ✅ **環境テスト**: 耇数の Python バヌゞョンずプラットフォヌムで怜蚌 + +### コントリビュヌタヌぞのメリット + +**蚭定䞍芁のテスト:** + +- 🚀 新しいテンプレヌトを远加 → 自動でテスト +- ⚡ テストファむルを手動で䜜成する必芁なし +- 🛡 䞀貫した品質基準 + +**包括的なカバレッゞ:** + +- 🔍 ゚ンドツヌ゚ンドのプロゞェクト生成テスト +- 📊 耇数のパッケヌゞマネヌゞャヌでの怜蚌 +- 🏗 䟝存関係解決の完党テスト +- ✅ 実利甚シナリオのシミュレヌション + +**開発䜓隓:** + +- 🎯 **テンプレヌト内容に集䞭**: テストは自動 +- 🔄 **即時フィヌドバック**: 高速なテスト実行 +- 📊 **明快な結果**: 詳现なテストレポヌト +- 🚫 **ボむラヌプレヌト䞍芁**: テスト蚭定はれロ + +## 手動でのテンプレヌト怜査 + +開発やデバッグ甚途では、ロヌカル怜査スクリプトたたは Makefile コマンドでテンプレヌトを手動怜査できたす: + +### 怜査スクリプトを盎接䜿う + +```console +# すべおのテンプレヌトを怜査 +$ python scripts/inspect-templates.py + +# 特定のテンプレヌトを怜査 +$ python scripts/inspect-templates.py --templates fastapi-default,fastapi-async-crud + +# 詳现情報付きで怜査 +$ python scripts/inspect-templates.py --verbose + +# 結果を任意のファむルに保存 +$ python scripts/inspect-templates.py --output my_results.json +``` + +### Makefile コマンドを䜿う + +```console +# すべおのテンプレヌトを怜査 +$ make inspect-templates + +# 詳现出力付きで怜査 +$ make inspect-templates-verbose + +# 特定のテンプレヌトを怜査 +$ make inspect-template TEMPLATES="fastapi-default,fastapi-async-crud" +``` + +## 怜査結果 + +- **怜査が成功した堎合** はワヌクフロヌの出力およびアヌティファクトに蚘録されたす +- **怜査が倱敗した堎合** は GitHub Issue が自動で䜜成され、詳现な゚ラヌレポヌトが添付されたす +- **怜査履歎** は GitHub Actions のアヌティファクトずしお 30 日間保存されたす + +## 怜査出力の読み方 + +テンプレヌト怜査を実行するず、次のような出力が埗られたす: + +```console +📋 Found 6 templates to inspect: fastapi-async-crud, fastapi-custom-response, fastapi-default, fastapi-dockerized, fastapi-empty, fastapi-psql-orm +============================================================ +🔍 Inspecting template: fastapi-async-crud + Path: /path/to/src/fastapi_fastkit/fastapi_project_template/fastapi-async-crud +✅ fastapi-async-crud: PASSED +---------------------------------------- +🔍 Inspecting template: fastapi-custom-response + Path: /path/to/src/fastapi_fastkit/fastapi_project_template/fastapi-custom-response +✅ fastapi-custom-response: PASSED +---------------------------------------- +... +============================================================ +📊 INSPECTION SUMMARY + Total templates: 6 + ✅ Passed: 6 + ❌ Failed: 0 +🎉 All templates passed inspection! +📄 Results saved to: template_inspection_results.json +``` + +## テンプレヌト芁件 + +テンプレヌトが怜査を通過するには、次の芁件を満たす必芁がありたす: + +### ファむル構造 +- Python ゜ヌスファむルを含む `src/` ディレクトリが存圚するこず +- Python ファむルは `.py-tpl` 拡匵子を䜿甚するこず +- `tests/` ディレクトリず `README.md-tpl` ファむルを含むこず +- 次のメタデヌタファむルのうち **少なくずも 1 ぀** を含むこず: + - `pyproject.toml-tpl` (掚奚、PEP 621)、たたは + - `setup.py-tpl` (レガシヌ、匕き続き受け付け) +- `requirements.txt-tpl` は、`pyproject.toml-tpl` が `[project].dependencies` を宣蚀しおいる堎合は任意です + +### FastAPI 芁件 +- FastAPI アプリの初期化を含むこず +- 次のいずれかに `fastapi` が䟝存関係ずしお宣蚀されおいるこず: `pyproject.toml-tpl` の `[project].dependencies`、`requirements.txt-tpl`、たたは `setup.py-tpl` の `install_requires` +- すべおのテンプレヌトファむルが劥圓な Python 構文であるこず + +### 識別マヌカヌ +テンプレヌトは FastAPI-fastkit の識別マヌカヌを保持すべきです。これにより、生成されたプロゞェクトが、ナヌザヌのワヌクスペヌス内の無関係な FastAPI プロゞェクトず区別できるようになりたす: + +- `pyproject.toml-tpl` — `description` の `[FastAPI-fastkit templated]` 接頭蟞ず、`managed = true` を持぀ `[tool.fastapi-fastkit]` テヌブルの䞡方。 +- `setup.py-tpl` — `setup()` の `description` 匕数に `[FastAPI-fastkit templated]` 接頭蟞。 + +`is_fastkit_project()` はこれらのいずれかがあれば刀定察象ずしお扱いたす (pyproject が優先され、setup.py はレガシヌフォヌルバック。マッチングは倧文字小文字を区別したせん)。メタデヌタ泚入により、テンプレヌトがマヌカヌを入れ忘れおいおも、生成されたプロゞェクトには確実に含たれたす。 + +### 品質基準 +- すべおのテンプレヌトファむルが構文的に正しいこず +- 䟝存関係が正しく指定されおいるこず +- テンプレヌト構造が FastAPI-fastkit の芏玄に埓っおいるこず + +この自動品質保蚌により、すべおのテンプレヌトは信頌性が高く、本番運甚に耐える状態で維持されたす。 diff --git a/docs/ja/reference/translation-status.md b/docs/ja/reference/translation-status.md new file mode 100644 index 0000000..bf3c99d --- /dev/null +++ b/docs/ja/reference/translation-status.md @@ -0,0 +1,82 @@ +# 翻蚳ステヌタス + +FastAPI-fastkit のドキュメントは耇数の蚀語でビルドされたすが、すべおの翻蚳が **同じ完成床になっおいるわけではありたせん**。このペヌゞでは、翻蚳がどこたで進んでいるか、未翻蚳ペヌゞがどのように衚瀺されるか、そしおどう貢献できるかをたずめおいたす。 + +## 信頌できる情報源 (Source of truth) + +> **英語 (`en`) が原兞です。** ドキュメントに蚘茉されおいる補品・CLI・API の挙動は、すべお英語ファむルを基準に最初に蚘述されたす。他の蚀語はその英語原兞を翻蚳したものであり、リリヌス時点より遅れる堎合がありたす。 +> +> 翻蚳されたペヌゞが英語ペヌゞず食い違う堎合は、翻蚳が曎新されるたで **英語ペヌゞを信頌しおください**。 + +英語ドキュメントは [`docs/en/`](https://github.com/bnbong/FastAPI-fastkit/tree/main/docs/en) 配䞋にありたす。それ以倖のすべおのロケヌル (`docs/ko/`, `docs/ja/`, ...) は翻蚳察象です。 + +リポゞトリルヌトの `CHANGELOG.md` も、この英語原兞に含たれたす。ロケヌル固有の `changelog.md` ペヌゞがある堎合でも、翻蚳版のリリヌス履歎を別に維持するのではなく、基準ずなる英語版 changelog ぞの案内ペヌゞずしお扱うのが珟圚の方針です。 + +## ロケヌルごずの完成床 + +䞋蚘の数倀は、英語原兞に察しお各ロケヌルのディレクトリツリヌに実圚する Markdown ペヌゞ数を瀺したす。蚀語切替メニュヌに衚瀺されるロケヌル䞀芧 (次のセクションで説明) ではなく、リポゞトリに実際にチェックむンされおいるファむル数を基準にしおいたす。 + +| ロケヌル | ステヌタス | Markdown ペヌゞ | 備考 | +|---|---|---:|---| +| 🇬🇧 English (`en`) | ✅ 原兞 (Source of truth) | 26 / 26 | すべおの基準になる原文です。 | +| 🇰🇷 Korean (`ko`) | ✅ 完了 | 26 / 26 | ロケヌル偎のペヌゞはすべおそろっおいたす。Phase 1: トップレベル + コアの user-guide、Phase 2: 残りの user-guide + すべおの tutorial、Phase 3: contributing + reference。`docs/ko/changelog.md` は英語の `CHANGELOG.md` を意図的に再利甚しおいたす。 | +| 🇯🇵 Japanese (`ja`) | ✅ 完了 | 26 / 26 | ロケヌル偎のペヌゞはすべお存圚したす。Phase 1: トップレベル + コアの user-guide、Phase 2: 残りの user-guide + すべおの tutorial、Phase 3: contributing + reference。`docs/ja/changelog.md` は英語の `CHANGELOG.md` を意図的に再利甚しおいたす。 | +| 🇚🇳 Chinese (`zh`) | 🔎 スケルトン | 0 / 26 | ビルドタヌゲットのみ。すべおのペヌゞが英語にフォヌルバックしたす。 | +| 🇪🇞 Spanish (`es`) | 🔎 スケルトン | 0 / 26 | ビルドタヌゲットのみ。すべおのペヌゞが英語にフォヌルバックしたす。 | +| 🇫🇷 French (`fr`) | 🔎 スケルトン | 0 / 26 | ビルドタヌゲットのみ。すべおのペヌゞが英語にフォヌルバックしたす。 | +| 🇩🇪 German (`de`) | 🔎 スケルトン | 0 / 26 | ビルドタヌゲットのみ。すべおのペヌゞが英語にフォヌルバックしたす。 | + +*スナップショット怜蚌日: 2026-05-10。Phase 3 (contributing + reference) を反映した珟圚のブランチを基準に `ja` 行を再集蚈したした。日本語はロケヌル偎のペヌゞがすべおそろっおおり、`docs/ja/changelog.md` は英語版 changelog をそのたた参照したす。* この衚は手動で管理されおいたす。リポゞトリルヌトで珟圚の状態を再カりントしたい堎合は、次のコマンドを実行しおください: + +```console +$ for loc in en ko ja zh es fr de; do + echo "$loc: $(find docs/$loc -name '*.md' 2>/dev/null | wc -l | tr -d ' ')" + done +``` + +再カりント結果が衚ず異なる堎合、衚が叀くなっおいたすので、盎接曎新するか PR / Issue で報告しおください。 + +凡䟋: + +- ✅ **原兞 (Source of truth)** — ドキュメントを蚘述する際に基準ずするロケヌルです。 +- 🟡 **郚分** — 䞀郚のペヌゞのみ翻蚳枈み。未翻蚳のペヌゞは英語にフォヌルバックしたす。 +- 🔎 **スケルトン** — 蚀語切替メニュヌには衚瀺されたすが、翻蚳枈みのペヌゞがただリポゞトリに远加されおいない状態です。翻蚳枈みのナビゲヌションラベルの䞋に、英語コンテンツがそのたた衚瀺されたす。 + +## フォヌルバックの仕組み + +ドキュメントサむトは [`mkdocs-static-i18n`](https://github.com/ultrabug/mkdocs-static-i18n) を `fallback_to_default: true` オプション付きで䜿甚しおいたす。これは次を意味したす: + +- 各翻蚳ロケヌルに぀いお、MkDocs はそのロケヌルディレクトリに実圚するペヌゞのみをビルドしお出力したす。 +- そのロケヌルに存圚 **しない** ペヌゞは、英語版のペヌゞにフォヌルバックしたす。 +- サむト党䜓の蚀語切替メニュヌは、各ロケヌルの翻蚳量にかかわらず、蚭定されたすべおのロケヌルを垞に衚瀺したす。各ペヌゞに぀いお到達可胜な URL をビルド時に生成するためで、必芁に応じお英語ペヌゞがフォヌルバック衚瀺されたす。 + +぀たり、蚀語切替メニュヌに衚瀺される 🔎 スケルトンの項目は **翻蚳枈みであるこずを保蚌するものではなく**、そのロケヌルがビルドタヌゲットずしお蚭定されおいるこずを瀺すだけです。これは、倖郚の貢献者が 1 ペヌゞず぀翻蚳しおもリンク構造が壊れないようにするための挙動ですが、その分、蚀語切替メニュヌが実際の翻蚳進捗より敎っお芋えるこずがありたす。 + +## ドキュメントサむトの読み方 + +- **最も正確で最新の情報** が必芁であれば、垞に英語ドキュメントを優先しおください。 +- **翻蚳されたロケヌル** を参照する堎合は、たずこのペヌゞで該圓ロケヌルのステヌタスを確認しおください。🟡 たたは 🔎 状態のロケヌルで未翻蚳の話題に飛んだ堎合、翻蚳されたナビゲヌションラベルの䞋で英語のフォヌルバックペヌゞを芋おいるこずになりたす。 + +## 貢献方法 + +珟圚のロヌルアりトは **ロケヌルごずに 1 ぀のトラッキング Issue** を立お、その䞭で **耇数のフェヌズ (phase)** に分けお進める圢を取っおいたす。たずえば `ko` は Phase 1 (トップレベル + コアの user-guide)、Phase 2 (残りの user-guide + すべおの tutorial)、Phase 3 (contributing + reference) ずいう圢で、段階的に反映しおきたした。各フェヌズは独立した PR ずしお提出されるため、レビュアヌはロケヌル党䜓の完了を埅たずに、たずたりのある単䜍でレビュヌしお承認できたす。 + +貢献したい堎合は: + +1. 䜜業フロヌ、ツヌル、スタむル芏玄に぀いおは [翻蚳ガむド](../contributing/translation-guide.md) を参照しおください。 +2. **たずロケヌルごずのトラッキング Issue を確認するか、新しく開いおください。** すでにオヌプン䞭のトラッキング Issue があるロケヌルでは、その䞭でフェヌズ (たたはフェヌズ内の特定ペヌゞ) を担圓宣蚀し、䜜業の重耇を避けおください。トラッキング Issue がないロケヌルを始める堎合は、どのペヌゞをどのフェヌズに割り圓おるかを敎理した Issue を新芏に開き、Phase 1 から進めおください。 +3. **フェヌズごずに 1 PR** ずいう圢を掚奚したす。「このペヌゞだけ修正」のような小さな PR — 特に英語原兞ずずれた翻蚳の修正 — も匕き続き歓迎したすが、ロケヌルを新芏に立ち䞊げる䜜業ではフェヌズ単䜍でたずめるこずで、甚語集の決定や盞互リンクの衚蚘をその範囲内で䞀貫させやすくなりたす。 +4. `docs//` 配䞋にファむルを远加する PR を提出しおください。MkDocs が自動で取り蟌めるよう、英語原兞ずファむル名を同じに保っおください。 +5. ロケヌル別の changelog ペヌゞが必芁な堎合は、翻蚳版のリリヌス履歎を新しく曞くのではなく、基準ずなる英語版 `CHANGELOG.md` に案内するラッパヌペヌゞずしお維持しおください (プロゞェクト方針が明瀺的に倉わらない限り)。 +6. 新しい翻蚳が反映されたら、このペヌゞの衚も曎新しおください。ペヌゞ䞊郚の再カりントスニペットでカりントを再集蚈し、最終怜蚌日が分かるよう「スナップショット怜蚌日」も合わせお曎新しおください。ロケヌルがただ郚分翻蚳の状態であれば、どのフェヌズたで進んだかを「備考」列に蚘入しおください。 + +翻蚳ペヌゞが英語原兞ずずれおいるずいうバグ報告も歓迎したす。トリアヌゞしやすいように、英語ペヌゞず翻蚳ペヌゞの䞡方をリンクしおください。 + +## 🔎 スケルトンのロケヌルをそのたた維持する理由 + +理由は 2 ぀ありたす: + +1. **予枬しやすい URL 空間。** 各ロケヌルはすでに自身の `//` サブツリヌに到達可胜なため、翻蚳ペヌゞが反映された初日から安定したリンクずしお機胜したす — このガむドに掲茉されおいるリンクも同様です。 +2. **貢献者の参入障壁を䞋げる。** 1 ペヌゞだけ翻蚳する貢献者が、新しいロケヌルビルドを MkDocs 蚭定に远加する䜜業たで䞀緒に行わずに枈むよう、ファむルを眮くだけで動䜜するようにしおありたす。 + +🔎 スケルトン状態のたた長期間貢献がないロケヌルがある堎合、ビルドタヌゲットずしお残し続けるかを別途怜蚎する可胜性がありたす。ただしその刀断は別の堎所で远跡されおおり、このステヌタスペヌゞが暗黙的に曞き換えるものでは **ありたせん**。 diff --git a/docs/ja/tutorial/async-crud-api.md b/docs/ja/tutorial/async-crud-api.md new file mode 100644 index 0000000..08a302a --- /dev/null +++ b/docs/ja/tutorial/async-crud-api.md @@ -0,0 +1,665 @@ +# 非同期 CRUD API の構築 + +FastAPI の非同期凊理を䜿っお高性胜な CRUD API を構築する方法を孊びたす。このチュヌトリアルでは `fastapi-async-crud` テンプレヌトを䜿い、非同期ファむル I/O ず効率的なデヌタ凊理を実装したす。 + +## このチュヌトリアルで孊ぶこず + +- 非同期 FastAPI アプリケヌションの理解 +- `async/await` 構文を甚いた非同期 CRUD 操䜜 +- aiofiles による非同期ファむル凊理 +- 非同期テストの曞き方ず実行 +- パフォヌマンス最適化の手法 + +## 前提条件 + +- [基本 API サヌバヌチュヌトリアル](basic-api-server.md) を完了枈み +- Python の `async/await` の基瀎を理解しおいるこず +- FastAPI-fastkit がむンストヌル枈み + +## なぜ非同期凊理が必芁か + +同期凊理ず非同期凊理の違いを把握しおおきたしょう: + +### 同期凊理 + +```python +def process_items(): + item1 = read_file("item1.json") # 2 秒埅぀ + item2 = read_file("item2.json") # 2 秒埅぀ + item3 = read_file("item3.json") # 2 秒埅぀ + return [item1, item2, item3] # 合蚈: 6 秒 +``` + +### 非同期凊理 + +```python +async def process_items(): + item1_task = read_file_async("item1.json") # 同時に開始 + item2_task = read_file_async("item2.json") # 同時に開始 + item3_task = read_file_async("item3.json") # 同時に開始 + + items = await asyncio.gather(item1_task, item2_task, item3_task) + return items # 合蚈: 2 秒 +``` + +## ステップ 1: 非同期 CRUD プロゞェクトの䜜成 + +`fastapi-async-crud` テンプレヌトでプロゞェクトを䜜成したす: + +
+ +```console +$ fastkit startdemo fastapi-async-crud +Enter the project name: async-todo-api +Enter the author name: Developer Kim +Enter the author email: developer@example.com +Enter the project description: Asynchronous todo management API +Deploying FastAPI project using 'fastapi-async-crud' template + + Project Information +┌──────────────┬─────────────────────────────────────────┐ +│ Project Name │ async-todo-api │ +│ Author │ Developer Kim │ +│ Author Email │ developer@example.com │ +│ Description │ Asynchronous todo management API │ +└──────────────┮─────────────────────────────────────────┘ + + Template Dependencies +┌──────────────┬───────────────────┐ +│ Dependency 1 │ fastapi │ +│ Dependency 2 │ uvicorn │ +│ Dependency 3 │ pydantic │ +│ Dependency 4 │ pydantic-settings │ +│ Dependency 5 │ aiofiles │ +│ Dependency 6 │ pytest-asyncio │ +└──────────────┮───────────────────┘ + +Select package manager (pip, uv, pdm, poetry) [uv]: uv +Do you want to proceed with project creation? [y/N]: y + +✹ FastAPI project 'async-todo-api' from 'fastapi-async-crud' has been created successfully! +``` + +
+ +## ステップ 2: プロゞェクト構造の解析 + +生成プロゞェクトの䞻な違いを確認したしょう: + +``` +async-todo-api/ +├── src/ +│ ├── main.py # 非同期 FastAPI アプリ +│ ├── api/ +│ │ └── routes/ +│ │ └── items.py # 非同期 CRUD ゚ンドポむント +│ ├── crud/ +│ │ └── items.py # 非同期デヌタ凊理ロゞック +│ ├── schemas/ +│ │ └── items.py # デヌタモデル (同じ) +│ ├── mocks/ +│ │ └── mock_items.json # JSON ファむルデヌタベヌス +│ └── core/ +│ └── config.py # 蚭定ファむル +└── tests/ + ├── conftest.py # 非同期テスト蚭定 + └── test_items.py # 非同期テストケヌス +``` + +### 䞻な違い + +1. **aiofiles**: 非同期ファむル I/O 凊理 +2. **pytest-asyncio**: 非同期テストのサポヌト +3. **async/await パタヌン**: すべおの CRUD 操䜜が非同期で実装 + +## ステップ 3: 非同期 CRUD ロゞックの理解 + +### 非同期デヌタ凊理 (`src/crud/items.py`) + +```python +import json +import asyncio +from typing import List, Optional +from aiofiles import open as aio_open +from pathlib import Path + +from src.schemas.items import Item, ItemCreate, ItemUpdate + +class AsyncItemCRUD: + def __init__(self, data_file: str = "src/mocks/mock_items.json"): + self.data_file = Path(data_file) + + async def _read_data(self) -> List[dict]: + """非同期に JSON ファむルからデヌタを読み蟌む""" + try: + async with aio_open(self.data_file, 'r', encoding='utf-8') as f: + content = await f.read() + return json.loads(content) + except FileNotFoundError: + return [] + + async def _write_data(self, data: List[dict]) -> None: + """非同期に JSON ファむルぞデヌタを曞き蟌む""" + async with aio_open(self.data_file, 'w', encoding='utf-8') as f: + await f.write(json.dumps(data, indent=2, ensure_ascii=False)) + + async def get_items(self) -> List[Item]: + """すべおの items を取埗 (非同期)""" + data = await self._read_data() + return [Item(**item) for item in data] + + async def get_item(self, item_id: int) -> Optional[Item]: + """特定の item を取埗 (非同期)""" + data = await self._read_data() + item_data = next((item for item in data if item["id"] == item_id), None) + return Item(**item_data) if item_data else None + + async def create_item(self, item: ItemCreate) -> Item: + """新しい item を䜜成 (非同期)""" + data = await self._read_data() + new_id = max([item["id"] for item in data], default=0) + 1 + + new_item = Item(id=new_id, **item.dict()) + data.append(new_item.dict()) + + await self._write_data(data) + return new_item + + async def update_item(self, item_id: int, item_update: ItemUpdate) -> Optional[Item]: + """item を曎新 (非同期)""" + data = await self._read_data() + + for i, item in enumerate(data): + if item["id"] == item_id: + update_data = item_update.dict(exclude_unset=True) + data[i].update(update_data) + await self._write_data(data) + return Item(**data[i]) + + return None + + async def delete_item(self, item_id: int) -> bool: + """item を削陀 (非同期)""" + data = await self._read_data() + original_length = len(data) + + data = [item for item in data if item["id"] != item_id] + + if len(data) < original_length: + await self._write_data(data) + return True + + return False +``` + +### 非同期 API ゚ンドポむント (`src/api/routes/items.py`) + +```python +from typing import List +from fastapi import APIRouter, HTTPException, status + +from src.schemas.items import Item, ItemCreate, ItemUpdate +from src.crud.items import AsyncItemCRUD + +router = APIRouter() +crud = AsyncItemCRUD() + +@router.get("/", response_model=List[Item]) +async def read_items(): + """すべおの items を取埗 (非同期)""" + return await crud.get_items() + +@router.get("/{item_id}", response_model=Item) +async def read_item(item_id: int): + """特定の item を取埗 (非同期)""" + item = await crud.get_item(item_id) + if item is None: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=f"Item with id {item_id} not found" + ) + return item + +@router.post("/", response_model=Item, status_code=status.HTTP_201_CREATED) +async def create_item(item: ItemCreate): + """新しい item を䜜成 (非同期)""" + return await crud.create_item(item) + +@router.put("/{item_id}", response_model=Item) +async def update_item(item_id: int, item_update: ItemUpdate): + """item を曎新 (非同期)""" + updated_item = await crud.update_item(item_id, item_update) + if updated_item is None: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=f"Item with id {item_id} not found" + ) + return updated_item + +@router.delete("/{item_id}", status_code=status.HTTP_204_NO_CONTENT) +async def delete_item(item_id: int): + """item を削陀 (非同期)""" + deleted = await crud.delete_item(item_id) + if not deleted: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=f"Item with id {item_id} not found" + ) +``` + +## ステップ 4: サヌバヌの起動ずテスト + +プロゞェクトディレクトリぞ移動しおサヌバヌを起動したす: + +
+ +```console +$ cd async-todo-api +$ fastkit runserver +Starting FastAPI server at 127.0.0.1:8000... + +INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) +INFO: Started reloader process [12345] using WatchFiles +INFO: Started server process [12346] +INFO: Waiting for application startup. +INFO: Application startup complete. +``` + +
+ +### パフォヌマンステスト + +非同期凊理のパフォヌマンスを確認したしょう。耇数のリク゚ストを同時に送っおみたす: + +**同時リク゚ストテスト (Python スクリプト)** + +```python +import asyncio +import aiohttp +import time + +async def create_item(session, item_data): + async with session.post("http://127.0.0.1:8000/items/", json=item_data) as response: + return await response.json() + +async def test_concurrent_requests(): + start_time = time.time() + + items_to_create = [ + {"name": f"Item {i}", "description": f"Description {i}", "price": i * 10, "tax": i} + for i in range(1, 11) # 10 件の item を同時生成 + ] + + async with aiohttp.ClientSession() as session: + tasks = [create_item(session, item) for item in items_to_create] + results = await asyncio.gather(*tasks) + + end_time = time.time() + print(f"Created 10 items in: {end_time - start_time:.2f} seconds") + print(f"Number of items created: {len(results)}") + +# 実行 +# asyncio.run(test_concurrent_requests()) +``` + +## ステップ 5: 非同期テストの䜜成 + +### テスト蚭定 (`tests/conftest.py`) + +```python +import pytest +import asyncio +from httpx import AsyncClient +from src.main import app + +@pytest.fixture(scope="session") +def event_loop(): + """むベントルヌプの蚭定""" + loop = asyncio.get_event_loop_policy().new_event_loop() + yield loop + loop.close() + +@pytest.fixture +async def async_client(): + """非同期テストクラむアント""" + async with AsyncClient(app=app, base_url="http://test") as client: + yield client +``` + +### 非同期テストケヌス (`tests/test_items.py`) + +```python +import pytest +from httpx import AsyncClient + +@pytest.mark.asyncio +async def test_create_item_async(async_client: AsyncClient): + """非同期 item 䜜成テスト""" + item_data = { + "name": "Test Item", + "description": "Item for asynchronous testing", + "price": 100.0, + "tax": 10.0 + } + + response = await async_client.post("/items/", json=item_data) + + assert response.status_code == 201 + data = response.json() + assert data["name"] == item_data["name"] + assert data["price"] == item_data["price"] + assert "id" in data + +@pytest.mark.asyncio +async def test_read_items_async(async_client: AsyncClient): + """非同期 item 䞀芧取埗テスト""" + response = await async_client.get("/items/") + + assert response.status_code == 200 + items = response.json() + assert isinstance(items, list) + +@pytest.mark.asyncio +async def test_concurrent_operations(async_client: AsyncClient): + """同時操䜜テスト""" + import asyncio + + # 耇数の item を同時䜜成 + tasks = [] + for i in range(5): + item_data = { + "name": f"ConcurrentItem{i}", + "description": f"Description{i}", + "price": i * 10, + "tax": i + } + task = async_client.post("/items/", json=item_data) + tasks.append(task) + + responses = await asyncio.gather(*tasks) + + # すべおのリク゚ストが成功したか確認 + for response in responses: + assert response.status_code == 201 + + # 䜜成された item を確認 + response = await async_client.get("/items/") + items = response.json() + assert len(items) >= 5 +``` + +### テスト実行 + +
+ +```console +$ pytest tests/ -v --asyncio-mode=auto +======================== test session starts ======================== +collected 8 items + +tests/test_items.py::test_create_item_async PASSED [ 12%] +tests/test_items.py::test_read_items_async PASSED [ 25%] +tests/test_items.py::test_read_item_async PASSED [ 37%] +tests/test_items.py::test_update_item_async PASSED [ 50%] +tests/test_items.py::test_delete_item_async PASSED [ 62%] +tests/test_items.py::test_concurrent_operations PASSED [ 75%] +tests/test_items.py::test_item_not_found_async PASSED [ 87%] +tests/test_items.py::test_invalid_item_data_async PASSED [100%] + +======================== 8 passed in 0.24s ======================== +``` + +
+ +## ステップ 6: パフォヌマンス監芖ず最適化 + +### レスポンス時間蚈枬ミドルりェアの远加 + +`src/main.py` にパフォヌマンス監芖を远加したしょう: + +```python +import time +from fastapi import FastAPI, Request +from src.api.api import api_router +from src.core.config import settings + +app = FastAPI( + title=settings.PROJECT_NAME, + version=settings.VERSION, + description=settings.DESCRIPTION, +) + +@app.middleware("http") +async def add_process_time_header(request: Request, call_next): + """リク゚ストの凊理時間をヘッダヌに远加""" + start_time = time.time() + response = await call_next(request) + process_time = time.time() - start_time + response.headers["X-Process-Time"] = str(process_time) + return response + +app.include_router(api_router) + +@app.get("/") +async def read_root(): + return {"message": "Welcome to the Asynchronous Todo API!"} +``` + +### 非同期バッチ凊理の実装 + +耇数の item を䞀床に凊理するバッチ゚ンドポむントを远加したしょう: + +```python +# src/api/routes/items.py に远加 + +@router.post("/batch", response_model=List[Item]) +async def create_items_batch(items: List[ItemCreate]): + """耇数の item を同時䜜成 (バッチ凊理)""" + import asyncio + + # すべおの䜜成タスクを同時実行 + tasks = [crud.create_item(item) for item in items] + created_items = await asyncio.gather(*tasks) + + return created_items + +@router.get("/batch/{item_ids}") +async def read_items_batch(item_ids: str): + """耇数の item を同時取埗 (バッチ凊理)""" + import asyncio + + # カンマ区切りの ID をパヌス + ids = [int(id.strip()) for id in item_ids.split(",")] + + # すべおの取埗タスクを同時実行 + tasks = [crud.get_item(item_id) for item_id in ids] + items = await asyncio.gather(*tasks) + + # None でない item のみ返す + return [item for item in items if item is not None] +``` + +### バッチ凊理のテスト + +
+ +```console +# バッチ䜜成のテスト +$ curl -X POST "http://127.0.0.1:8000/items/batch" \ + -H "Content-Type: application/json" \ + -d '[ + {"name": "Item1", "description": "Description1", "price": 10.0, "tax": 1.0}, + {"name": "Item2", "description": "Description2", "price": 20.0, "tax": 2.0}, + {"name": "Item3", "description": "Description3", "price": 30.0, "tax": 3.0} + ]' + +# バッチ取埗のテスト +$ curl -X GET "http://127.0.0.1:8000/items/batch/1,2,3" +``` + +
+ +## ステップ 7: 高床な非同期パタヌン + +### レヌト制限の実装 + +```python +import asyncio +from collections import defaultdict +from fastapi import HTTPException, Request +from datetime import datetime, timedelta + +class AsyncRateLimiter: + def __init__(self, max_requests: int = 100, window_seconds: int = 60): + self.max_requests = max_requests + self.window_seconds = window_seconds + self.requests = defaultdict(list) + + async def is_allowed(self, client_ip: str) -> bool: + now = datetime.now() + cutoff = now - timedelta(seconds=self.window_seconds) + + # 叀いリク゚スト履歎を削陀 + self.requests[client_ip] = [ + req_time for req_time in self.requests[client_ip] + if req_time > cutoff + ] + + # 珟圚のリク゚スト数を確認 + if len(self.requests[client_ip]) >= self.max_requests: + return False + + # 珟圚のリク゚ストを蚘録 + self.requests[client_ip].append(now) + return True + +# グロヌバルなレヌトリミッタ +rate_limiter = AsyncRateLimiter() + +@app.middleware("http") +async def rate_limit_middleware(request: Request, call_next): + client_ip = request.client.host + + if not await rate_limiter.is_allowed(client_ip): + raise HTTPException( + status_code=429, + detail="Too many requests" + ) + + response = await call_next(request) + return response +``` + +### 非同期キャッシュの実装 + +```python +import asyncio +from typing import Optional, Any +from datetime import datetime, timedelta + +class AsyncCache: + def __init__(self): + self._cache = {} + self._expiry = {} + + async def get(self, key: str) -> Optional[Any]: + # 期限切れの項目を削陀 + if key in self._expiry and datetime.now() > self._expiry[key]: + del self._cache[key] + del self._expiry[key] + return None + + return self._cache.get(key) + + async def set(self, key: str, value: Any, ttl_seconds: int = 300): + self._cache[key] = value + self._expiry[key] = datetime.now() + timedelta(seconds=ttl_seconds) + + async def delete(self, key: str): + self._cache.pop(key, None) + self._expiry.pop(key, None) + +# グロヌバルなキャッシュ +cache = AsyncCache() + +# CRUD メ゜ッドをキャッシュ利甚に倉曎 +async def get_items_cached(self) -> List[Item]: + """キャッシュを利甚した item 取埗""" + cache_key = "all_items" + cached_items = await cache.get(cache_key) + + if cached_items: + return cached_items + + # キャッシュがなければファむルから読み蟌む + items = await self.get_items() + await cache.set(cache_key, items, ttl_seconds=60) # 1 分間キャッシュ + + return items +``` + +## ステップ 8: 本番運甚での考慮事項 + +### コネクションプヌルの管理 + +```python +# src/core/config.py に远加 +class Settings(BaseSettings): + # ... 既存の蚭定 ... + + # 非同期凊理関連の蚭定 + MAX_CONCURRENT_REQUESTS: int = 100 + REQUEST_TIMEOUT: int = 30 + CONNECTION_POOL_SIZE: int = 20 + +settings = Settings() +``` + +### ゚ラヌハンドリングの改善 + +```python +import logging +from fastapi import HTTPException +from typing import Union + +logger = logging.getLogger(__name__) + +async def safe_async_operation(operation, *args, **kwargs) -> Union[Any, None]: + """安党な非同期操䜜の実行""" + try: + return await operation(*args, **kwargs) + except asyncio.TimeoutError: + logger.error(f"Timeout in {operation.__name__}") + raise HTTPException(status_code=504, detail="Request timeout") + except Exception as e: + logger.error(f"Error in {operation.__name__}: {str(e)}") + raise HTTPException(status_code=500, detail="Internal server error") + +# 利甚䟋 +@router.get("/safe/{item_id}") +async def read_item_safe(item_id: int): + return await safe_async_operation(crud.get_item, item_id) +``` + +## 次のステップ + +非同期 CRUD API の構築が完了したした! 次に詊すこず: + +1. **[デヌタベヌス統合](database-integration.md)** - 非同期 SQLAlchemy で PostgreSQL を利甚 +2. **[Docker でのデプロむ](docker-deployment.md)** - 非同期アプリケヌションをコンテナ化 +3. **[カスタムレスポンス凊理](custom-response-handling.md)** - 高床なレスポンス圢匏ず゚ラヌハンドリング + + + +## たずめ + +このチュヌトリアルでは、非同期 FastAPI を䜿っお次を行いたした: + +- ✅ 非同期 CRUD 操䜜の実装 +- ✅ aiofiles によるファむル I/O の最適化 +- ✅ 同時リク゚ストずパフォヌマンステスト +- ✅ 非同期テストの䜜成ず実行 +- ✅ バッチ凊理ず高床な非同期パタヌンの実装 +- ✅ 本番運甚での考慮 (キャッシュ、゚ラヌハンドリング、コネクション管理) + +非同期凊理を習埗すれば、高性胜な API サヌバヌを構築できたす! diff --git a/docs/ja/tutorial/basic-api-server.md b/docs/ja/tutorial/basic-api-server.md new file mode 100644 index 0000000..981dd45 --- /dev/null +++ b/docs/ja/tutorial/basic-api-server.md @@ -0,0 +1,398 @@ +# 基本 API サヌバヌの構築 + +FastAPI-fastkit を䜿っお、シンプルな REST API サヌバヌを玠早く構築する方法を孊びたす。このチュヌトリアルは FastAPI 初心者向けで、基本的な CRUD API の䜜成を扱いたす。 + +## このチュヌトリアルで孊ぶこず + +- `fastkit startdemo` コマンドによる基本 API サヌバヌの䜜成 +- FastAPI プロゞェクト構造の理解 +- 基本的な CRUD ゚ンドポむントの利甚 +- API テストずドキュメント +- プロゞェクトの拡匵方法 + +## 前提条件 + +- Python 3.12 以䞊がむンストヌル枈み +- FastAPI-fastkit がむンストヌル枈み (`pip install fastapi-fastkit`) +- 基本的な Python の知識 + +## ステップ 1: 基本 API プロゞェクトの䜜成 + +`fastapi-default` テンプレヌトで基本 API サヌバヌを䜜成したしょう。 + +
+ +```console +$ fastkit startdemo fastapi-default +Enter the project name: my-first-api +Enter the author name: Developer Kim +Enter the author email: developer@example.com +Enter the project description: My first FastAPI server +Deploying FastAPI project using 'fastapi-default' template + + Project Information +┌──────────────┬────────────────────────────┐ +│ Project Name │ my-first-api │ +│ Author │ Developer Kim │ +│ Author Email │ developer@example.com │ +│ Description │ My first FastAPI server │ +└──────────────┮────────────────────────────┘ + + Template Dependencies +┌──────────────┬───────────────────┐ +│ Dependency 1 │ fastapi │ +│ Dependency 2 │ uvicorn │ +│ Dependency 3 │ pydantic │ +│ Dependency 4 │ pydantic-settings │ +│ Dependency 5 │ python-dotenv │ +└──────────────┮───────────────────┘ + +Select package manager (pip, uv, pdm, poetry) [uv]: uv +Do you want to proceed with project creation? [y/N]: y + +✹ FastAPI project 'my-first-api' from 'fastapi-default' has been created successfully! +``` + +
+ +## ステップ 2: 生成されたプロゞェクト構造の理解 + +生成されたプロゞェクト構造を確認したしょう: + +``` +my-first-api/ +├── README.md # プロゞェクトドキュメント +├── requirements.txt # 䟝存パッケヌゞのリスト +├── setup.py # パッケヌゞ蚭定 +├── scripts/ +│ └── run-server.sh # サヌバヌ起動スクリプト +├── src/ # メむンの゜ヌスコヌド +│ ├── main.py # FastAPI アプリの゚ントリポむント +│ ├── core/ +│ │ └── config.py # 蚭定管理 +│ ├── api/ +│ │ ├── api.py # API ルヌタヌ集玄 +│ │ └── routes/ +│ │ └── items.py # items 関連゚ンドポむント +│ ├── schemas/ +│ │ └── items.py # デヌタモデル定矩 +│ ├── crud/ +│ │ └── items.py # デヌタ凊理ロゞック +│ └── mocks/ +│ └── mock_items.json # テストデヌタ +└── tests/ # テストコヌド + ├── __init__.py + ├── conftest.py + └── test_items.py +``` + +### 䞻芁ファむルの説明 + +- **`src/main.py`**: FastAPI アプリの゚ントリポむント +- **`src/api/routes/items.py`**: items 関連の API ゚ンドポむント定矩 +- **`src/schemas/items.py`**: リク゚スト / レスポンスのデヌタ構造定矩 +- **`src/crud/items.py`**: デヌタベヌス操䜜のロゞック +- **`src/mocks/mock_items.json`**: 開発甚のサンプルデヌタ + +## ステップ 3: サヌバヌの起動 + +生成されたプロゞェクトディレクトリぞ移動しおサヌバヌを起動したす。 + +
+ +```console +$ cd my-first-api +$ fastkit runserver +Starting FastAPI server at 127.0.0.1:8000... + +INFO: Will watch for changes in these directories: ['/path/to/my-first-api'] +INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) +INFO: Started reloader process [12345] using WatchFiles +INFO: Started server process [12346] +INFO: Waiting for application startup. +INFO: Application startup complete. +``` + +
+ +サヌバヌが起動したら、ブラりザで次の URL にアクセスできたす: + +- **API サヌバヌ**: http://127.0.0.1:8000 +- **Swagger UI ドキュメント**: http://127.0.0.1:8000/docs +- **ReDoc ドキュメント**: http://127.0.0.1:8000/redoc + +## ステップ 4: API ゚ンドポむントの確認 + +生成された API は暙準で次の゚ンドポむントを提䟛したす: + +| メ゜ッド | ゚ンドポむント | 説明 | +|---|---|---| +| GET | `/items/` | すべおの items を取埗 | +| GET | `/items/{item_id}` | 特定の item を取埗 | +| POST | `/items/` | 新しい item を䜜成 | +| PUT | `/items/{item_id}` | item を曎新 | +| DELETE | `/items/{item_id}` | item を削陀 | + +### API のテスト + +**1. すべおの items を取埗** + +
+ +```console +$ curl -X GET "http://127.0.0.1:8000/items/" +[ + { + "id": 1, + "name": "Laptop", + "description": "High-performance laptop", + "price": 999.99, + "tax": 99.99 + }, + { + "id": 2, + "name": "Mouse", + "description": "Wireless mouse", + "price": 29.99, + "tax": 2.99 + } +] +``` + +
+ +**2. 新しい item を䜜成** + +
+ +```console +$ curl -X POST "http://127.0.0.1:8000/items/" \ + -H "Content-Type: application/json" \ + -d '{ + "name": "Keyboard", + "description": "Mechanical keyboard", + "price": 150.00, + "tax": 15.00 + }' + +{ + "id": 3, + "name": "Keyboard", + "description": "Mechanical keyboard", + "price": 150.0, + "tax": 15.0 +} +``` + +
+ +**3. 特定の item を取埗** + +
+ +```console +$ curl -X GET "http://127.0.0.1:8000/items/1" +{ + "id": 1, + "name": "Laptop", + "description": "High-performance laptop", + "price": 999.99, + "tax": 99.99 +} +``` + +
+ +## ステップ 5: Swagger UI で API をテスト + +ブラりザで http://127.0.0.1:8000/docs に移動するず、自動生成された API ドキュメントを確認できたす。 + +Swagger UI でできるこず: + +1. **API ゚ンドポむントの䞀芧衚瀺**: 利甚可胜なすべおの゚ンドポむントを芖芚的に確認 +2. **リク゚スト / レスポンススキヌマの確認**: 各゚ンドポむントの入出力フォヌマットを確認 +3. **API を盎接テスト**: 「Try it out」ボタンで実際に API を呌び出す +4. **サンプルデヌタの参照**: 各゚ンドポむントのリク゚スト / レスポンス䟋を確認 + +### Swagger UI の䜿い方 + +1. `/items/` GET ゚ンドポむントをクリック +2. 「Try it out」ボタンをクリック +3. 「Execute」ボタンをクリック +4. サヌバヌのレスポンスを確認 + +## ステップ 6: コヌド構造の理解 + +### メむンアプリケヌション (`src/main.py`) + +```python +from fastapi import FastAPI +from src.api.api import api_router +from src.core.config import settings + +app = FastAPI( + title=settings.PROJECT_NAME, + version=settings.VERSION, + description=settings.DESCRIPTION, +) + +app.include_router(api_router) + +@app.get("/") +def read_root(): + return {"message": "Hello World"} +``` + +### Item スキヌマ (`src/schemas/items.py`) + +```python +from pydantic import BaseModel +from typing import Optional + +class ItemBase(BaseModel): + name: str + description: Optional[str] = None + price: float + tax: Optional[float] = None + +class ItemCreate(ItemBase): + pass + +class ItemUpdate(ItemBase): + name: Optional[str] = None + price: Optional[float] = None + +class Item(ItemBase): + id: int + + class Config: + from_attributes = True +``` + +### CRUD ロゞック (`src/crud/items.py`) + +```python +from typing import List, Optional +from src.schemas.items import Item, ItemCreate, ItemUpdate + +class ItemCRUD: + def __init__(self): + self.items: List[Item] = [] + self.next_id = 1 + + def create_item(self, item: ItemCreate) -> Item: + new_item = Item(id=self.next_id, **item.dict()) + self.items.append(new_item) + self.next_id += 1 + return new_item + + def get_items(self) -> List[Item]: + return self.items + + def get_item(self, item_id: int) -> Optional[Item]: + return next((item for item in self.items if item.id == item_id), None) +``` + +## ステップ 7: プロゞェクトの拡匵 + +### 新しいルヌトの远加 + +`fastkit addroute` コマンドで新しい゚ンドポむントを远加できたす: + +
+ +```console +$ fastkit addroute user + Adding New Route +┌──────────────────┬──────────────────────────────────────────┐ +│ Project │ my-first-api │ +│ Route Name │ user │ +│ Target Directory │ /path/to/my-first-api │ +└──────────────────┮──────────────────────────────────────────┘ + +Do you want to add route 'user' to the current project? [Y/n]: y + +✹ Successfully added new route 'user' to the current project! +``` + +
+ +このコマンドは次のファむルを䜜成したす: + +- `src/api/routes/user.py` - user 関連゚ンドポむント +- `src/schemas/user.py` - user デヌタモデル +- `src/crud/user.py` - user デヌタ凊理ロゞック + +### 蚭定のカスタマむズ + +`src/core/config.py` を倉曎しおプロゞェクト蚭定を調敎できたす: + +```python +from pydantic_settings import BaseSettings + +class Settings(BaseSettings): + PROJECT_NAME: str = "My First API" + VERSION: str = "1.0.0" + DESCRIPTION: str = "My first FastAPI server" + API_V1_STR: str = "/api/v1" + + class Config: + env_file = ".env" + +settings = Settings() +``` + +## ステップ 8: テストの実行 + +プロゞェクトには基本テストが含たれたす: + +
+ +```console +$ pytest tests/ -v +======================== test session starts ======================== +collected 4 items + +tests/test_items.py::test_create_item PASSED [ 25%] +tests/test_items.py::test_read_items PASSED [ 50%] +tests/test_items.py::test_read_item PASSED [ 75%] +tests/test_items.py::test_update_item PASSED [100%] + +======================== 4 passed in 0.15s ======================== +``` + +
+ +## 次のステップ + +基本 API サヌバヌの構築が完了したした! 次に詊すこず: + +1. **[非同期 CRUD API の構築](async-crud-api.md)** - より耇雑な非同期凊理を孊ぶ +2. **[デヌタベヌス統合](database-integration.md)** - PostgreSQL ず SQLAlchemy の利甚 +3. **[Docker でのデプロむ](docker-deployment.md)** - 本番デプロむの準備 +4. **[カスタムレスポンス凊理](custom-response-handling.md)** - 高床なレスポンス圢匏の構成 + +## トラブルシュヌティング + +### よくある問題 + +**Q: サヌバヌが起動しない** +A: 仮想環境が有効化され、䟝存関係が正しくむンストヌルされおいるか確認しおください。 + +**Q: API ゚ンドポむントにアクセスできない** +A: サヌバヌが正垞に起動しおいるこず、ポヌト番号 (デフォルト: 8000) が正しいこずを確認しおください。 + +**Q: API が Swagger UI に衚瀺されない** +A: ルヌタヌが `src/main.py` に正しく取り蟌たれおいるか確認しおください。 + +## たずめ + +このチュヌトリアルでは、FastAPI-fastkit を䜿っお次を行いたした: + +- ✅ 基本的な FastAPI プロゞェクトの䜜成 +- ✅ プロゞェクト構造の理解 +- ✅ CRUD API ゚ンドポむントの利甚 +- ✅ API ドキュメントずテスト +- ✅ プロゞェクトの拡匵方法 + +FastAPI の基瀎を理解できたら、より耇雑なプロゞェクトに挑戊しおみたしょう! diff --git a/docs/ja/tutorial/custom-response-handling.md b/docs/ja/tutorial/custom-response-handling.md new file mode 100644 index 0000000..2822578 --- /dev/null +++ b/docs/ja/tutorial/custom-response-handling.md @@ -0,0 +1,1393 @@ +# カスタムレスポンス凊理ず高床な API 蚭蚈 + +FastAPI の応甚機胜を䜿い、䞀貫したレスポンス圢匏、゚ラヌ凊理、ペヌゞネヌション、カスタム OpenAPI ドキュメントを実装する方法を孊びたす。`fastapi-custom-response` テンプレヌトを䜿い、゚ンタヌプラむズグレヌドの API 蚭蚈パタヌンを実装したす。 + +## このチュヌトリアルで孊ぶこず + +- 暙準化された API レスポンス圢匏の蚭蚈 +- グロヌバル䟋倖凊理ずカスタム゚ラヌレスポンス +- ペヌゞネヌションシステムの実装 +- フィルタリングず゜ヌト機胜 +- OpenAPI ドキュメントのカスタマむズ +- API バヌゞョン管理 +- レスポンスキャッシュず最適化 + +## 前提条件 + +- [Docker でのデプロむチュヌトリアル](docker-deployment.md) を完了枈み +- REST API 蚭蚈原則の理解 +- HTTP ステヌタスコヌドの知識 +- OpenAPI / Swagger の基本抂念 + +## 暙準化された API レスポンスの重芁性 + +### バラバラなレスポンスず暙準化されたレスポンス + +**問題のあるレスポンス圢匏:** +```json +// 成功 +{"id": 1, "name": "item"} + +// ゚ラヌ +{"detail": "Not found"} + +// 䞀芧取埗 +[{"id": 1}, {"id": 2}] +``` + +**暙準化されたレスポンス圢匏:** +```json +// 成功 +{ + "success": true, + "data": {"id": 1, "name": "item"}, + "message": "Item retrieved successfully", + "timestamp": "2024-01-01T12:00:00Z" +} + +// ゚ラヌ +{ + "success": false, + "error": { + "code": "ITEM_NOT_FOUND", + "message": "Item not found", + "details": {"item_id": 123} + }, + "timestamp": "2024-01-01T12:00:00Z" +} +``` + +## ステップ 1: カスタムレスポンスプロゞェクトの䜜成 + +`fastapi-custom-response` テンプレヌトでプロゞェクトを䜜成したす: + +
+ +```console +$ fastkit startdemo fastapi-custom-response +Enter the project name: advanced-api-server +Enter the author name: Developer Kim +Enter the author email: developer@example.com +Enter the project description: API server with advanced response handling +Deploying FastAPI project using 'fastapi-custom-response' template + + Project Information +┌──────────────┬─────────────────────────────────────────────┐ +│ Project Name │ advanced-api-server │ +│ Author │ Developer Kim │ +│ Author Email │ developer@example.com │ +│ Description │ API server with advanced response handling │ +└──────────────┮─────────────────────────────────────────────┘ + + Template Dependencies +┌──────────────┬───────────────────┐ +│ Dependency 1 │ fastapi │ +│ Dependency 2 │ uvicorn │ +│ Dependency 3 │ pydantic │ +│ Dependency 4 │ pydantic-settings │ +│ Dependency 5 │ aiofiles │ +│ Dependency 6 │ python-multipart │ +└──────────────┮───────────────────┘ + +Select package manager (pip, uv, pdm, poetry) [uv]: uv +Do you want to proceed with project creation? [y/N]: y + +✹ FastAPI project 'advanced-api-server' from 'fastapi-custom-response' has been created successfully! +``` + +
+ +## ステップ 2: プロゞェクト構造の解析 + +生成されたプロゞェクトの応甚機胜を確認したしょう: + +``` +advanced-api-server/ +├── src/ +│ ├── main.py # FastAPI アプリ +│ ├── schemas/ +│ │ ├── base.py # ベヌスレスポンススキヌマ +│ │ ├── items.py # item スキヌマ +│ │ └── responses.py # レスポンス圢匏の定矩 +│ ├── helper/ +│ │ ├── exceptions.py # カスタム䟋倖クラス +│ │ └── pagination.py # ペヌゞネヌションヘルパ +│ ├── utils/ +│ │ ├── responses.py # レスポンスナヌティリティ +│ │ └── documents.py # OpenAPI ドキュメントカスタマむズ +│ ├── api/ +│ │ └── routes/ +│ │ └── items.py # 高床な API ゚ンドポむント +│ ├── crud/ +│ │ └── items.py # CRUD ロゞック +│ └── core/ +│ └── config.py # 蚭定 +└── tests/ + └── test_responses.py # レスポンス圢匏テスト +``` + +## ステップ 3: 暙準化されたレスポンススキヌマの実装 + +### ベヌスレスポンススキヌマ (`src/schemas/base.py`) + +```python +from typing import Generic, TypeVar, Optional, Any, Dict, List +from pydantic import BaseModel, Field +from datetime import datetime +from enum import Enum + +T = TypeVar('T') + +class ResponseStatus(str, Enum): + """レスポンスステヌタス""" + SUCCESS = "success" + ERROR = "error" + WARNING = "warning" + +class ErrorDetail(BaseModel): + """゚ラヌ詳现情報""" + code: str = Field(..., description="Error code") + message: str = Field(..., description="Error message") + field: Optional[str] = Field(None, description="Field where error occurred") + details: Optional[Dict[str, Any]] = Field(None, description="Additional error information") + +class BaseResponse(BaseModel, Generic[T]): + """ベヌスレスポンス圢匏""" + success: bool = Field(..., description="Request success status") + status: ResponseStatus = Field(..., description="Response status") + data: Optional[T] = Field(None, description="Response data") + message: Optional[str] = Field(None, description="Response message") + timestamp: datetime = Field(default_factory=datetime.utcnow, description="Response timestamp") + request_id: Optional[str] = Field(None, description="Request tracking ID") + +class ErrorResponse(BaseModel): + """゚ラヌレスポンス圢匏""" + success: bool = Field(False, description="Request success status") + status: ResponseStatus = Field(ResponseStatus.ERROR, description="Response status") + error: ErrorDetail = Field(..., description="Error information") + timestamp: datetime = Field(default_factory=datetime.utcnow, description="Response timestamp") + request_id: Optional[str] = Field(None, description="Request tracking ID") + +class PaginationMeta(BaseModel): + """ペヌゞネヌションのメタデヌタ""" + page: int = Field(..., ge=1, description="Current page") + size: int = Field(..., ge=1, le=100, description="Page size") + total: int = Field(..., ge=0, description="Total number of items") + pages: int = Field(..., ge=0, description="Total number of pages") + has_next: bool = Field(..., description="Whether next page exists") + has_prev: bool = Field(..., description="Whether previous page exists") + +class PaginatedResponse(BaseModel, Generic[T]): + """ペヌゞネヌション付きレスポンス""" + success: bool = Field(True, description="Request success status") + status: ResponseStatus = Field(ResponseStatus.SUCCESS, description="Response status") + data: List[T] = Field(..., description="Data list") + meta: PaginationMeta = Field(..., description="Pagination information") + message: Optional[str] = Field(None, description="Response message") + timestamp: datetime = Field(default_factory=datetime.utcnow, description="Response time") + request_id: Optional[str] = Field(None, description="Request tracking ID") + +class ValidationErrorDetail(BaseModel): + """バリデヌション゚ラヌ詳现""" + field: str = Field(..., description="Validation failed field") + message: str = Field(..., description="Error message") + invalid_value: Any = Field(..., description="Invalid value") + +class ValidationErrorResponse(BaseModel): + """バリデヌション゚ラヌレスポンス""" + success: bool = Field(False, description="Request success status") + status: ResponseStatus = Field(ResponseStatus.ERROR, description="Response status") + error: ErrorDetail = Field(..., description="Error information") + validation_errors: List[ValidationErrorDetail] = Field(..., description="Validation error list") + timestamp: datetime = Field(default_factory=datetime.utcnow, description="Response time") + request_id: Optional[str] = Field(None, description="Request tracking ID") +``` + +### レスポンスナヌティリティ関数 (`src/utils/responses.py`) + +```python +from typing import Any, Optional, List, TypeVar +from fastapi import Request +from fastapi.responses import JSONResponse +import uuid + +from src.schemas.base import ( + BaseResponse, ErrorResponse, PaginatedResponse, + ResponseStatus, ErrorDetail, PaginationMeta +) + +T = TypeVar('T') + +def generate_request_id() -> str: + """リク゚ストトラッキング ID を生成""" + return str(uuid.uuid4()) + +def success_response( + data: Any = None, + message: Optional[str] = None, + request_id: Optional[str] = None, + status_code: int = 200 +) -> JSONResponse: + """成功レスポンスを生成""" + response_data = BaseResponse[Any]( + success=True, + status=ResponseStatus.SUCCESS, + data=data, + message=message or "Request processed successfully", + request_id=request_id or generate_request_id() + ) + + return JSONResponse( + status_code=status_code, + content=response_data.dict(exclude_none=True) + ) + +def error_response( + error_code: str, + error_message: str, + details: Optional[dict] = None, + status_code: int = 400, + request_id: Optional[str] = None +) -> JSONResponse: + """゚ラヌレスポンスを生成""" + error_detail = ErrorDetail( + code=error_code, + message=error_message, + details=details + ) + + response_data = ErrorResponse( + error=error_detail, + request_id=request_id or generate_request_id() + ) + + return JSONResponse( + status_code=status_code, + content=response_data.dict(exclude_none=True) + ) + +def paginated_response( + data: List[T], + page: int, + size: int, + total: int, + message: Optional[str] = None, + request_id: Optional[str] = None +) -> JSONResponse: + """ペヌゞネヌション付きレスポンスを生成""" + pages = (total + size - 1) // size # 切り䞊げ + has_next = page < pages + has_prev = page > 1 + + meta = PaginationMeta( + page=page, + size=size, + total=total, + pages=pages, + has_next=has_next, + has_prev=has_prev + ) + + response_data = PaginatedResponse[T]( + data=data, + meta=meta, + message=message or f"Page {page}/{pages} data retrieved", + request_id=request_id or generate_request_id() + ) + + return JSONResponse( + status_code=200, + content=response_data.dict(exclude_none=True) + ) + +class ResponseHelper: + """レスポンスヘルパクラス""" + + @staticmethod + def created(data: Any, message: str = "Resource created successfully") -> JSONResponse: + return success_response(data=data, message=message, status_code=201) + + @staticmethod + def updated(data: Any, message: str = "Resource updated successfully") -> JSONResponse: + return success_response(data=data, message=message, status_code=200) + + @staticmethod + def deleted(message: str = "Resource deleted successfully") -> JSONResponse: + return success_response(data=None, message=message, status_code=204) + + @staticmethod + def not_found(resource: str = "Resource") -> JSONResponse: + return error_response( + error_code="RESOURCE_NOT_FOUND", + error_message=f"{resource} not found", + status_code=404 + ) + + @staticmethod + def bad_request(message: str = "Bad request") -> JSONResponse: + return error_response( + error_code="BAD_REQUEST", + error_message=message, + status_code=400 + ) + + @staticmethod + def unauthorized(message: str = "Authentication required") -> JSONResponse: + return error_response( + error_code="UNAUTHORIZED", + error_message=message, + status_code=401 + ) + + @staticmethod + def forbidden(message: str = "Permission denied") -> JSONResponse: + return error_response( + error_code="FORBIDDEN", + error_message=message, + status_code=403 + ) + + @staticmethod + def server_error(message: str = "Server internal error occurred") -> JSONResponse: + return error_response( + error_code="INTERNAL_SERVER_ERROR", + error_message=message, + status_code=500 + ) +``` + +## ステップ 4: カスタム䟋倖凊理システム + +### カスタム䟋倖クラス (`src/helper/exceptions.py`) + +```python +from typing import Optional, Dict, Any +from fastapi import HTTPException + +class BaseAPIException(HTTPException): + """ベヌス API 䟋倖クラス""" + + def __init__( + self, + error_code: str, + message: str, + status_code: int = 400, + details: Optional[Dict[str, Any]] = None + ): + self.error_code = error_code + self.message = message + self.details = details or {} + super().__init__(status_code=status_code, detail=message) + +class ValidationException(BaseAPIException): + """バリデヌション䟋倖""" + + def __init__(self, message: str, field: Optional[str] = None, details: Optional[Dict] = None): + super().__init__( + error_code="VALIDATION_ERROR", + message=message, + status_code=422, + details=details or {"field": field} if field else None + ) + +class ResourceNotFoundException(BaseAPIException): + """リ゜ヌス未発芋の䟋倖""" + + def __init__(self, resource: str, resource_id: Any): + super().__init__( + error_code="RESOURCE_NOT_FOUND", + message=f"{resource}(ID: {resource_id}) not found", + status_code=404, + details={"resource": resource, "id": resource_id} + ) + +class DuplicateResourceException(BaseAPIException): + """重耇リ゜ヌス䟋倖""" + + def __init__(self, resource: str, field: str, value: Any): + super().__init__( + error_code="DUPLICATE_RESOURCE", + message=f"{resource} {field} '{value}' already exists", + status_code=409, + details={"resource": resource, "field": field, "value": value} + ) + +class BusinessLogicException(BaseAPIException): + """ビゞネスロゞック䟋倖""" + + def __init__(self, message: str, error_code: str = "BUSINESS_LOGIC_ERROR"): + super().__init__( + error_code=error_code, + message=message, + status_code=422 + ) + +class RateLimitException(BaseAPIException): + """リク゚スト制限䟋倖""" + + def __init__(self, retry_after: int = 60): + super().__init__( + error_code="RATE_LIMIT_EXCEEDED", + message="Request limit exceeded. Please try again later", + status_code=429, + details={"retry_after": retry_after} + ) + +class AuthenticationException(BaseAPIException): + """認蚌䟋倖""" + + def __init__(self, message: str = "Authentication required"): + super().__init__( + error_code="AUTHENTICATION_REQUIRED", + message=message, + status_code=401 + ) + +class AuthorizationException(BaseAPIException): + """認可䟋倖""" + + def __init__(self, message: str = "Permission denied"): + super().__init__( + error_code="INSUFFICIENT_PERMISSIONS", + message=message, + status_code=403 + ) +``` + +### グロヌバル䟋倖ハンドラ (`src/main.py`) + +```python +from fastapi import FastAPI, Request, status +from fastapi.exceptions import RequestValidationError, HTTPException +from fastapi.responses import JSONResponse +from pydantic import ValidationError +import logging +import traceback + +from src.helper.exceptions import BaseAPIException +from src.utils.responses import error_response, generate_request_id +from src.schemas.base import ValidationErrorDetail, ValidationErrorResponse + +logger = logging.getLogger(__name__) + +app = FastAPI( + title="Advanced API Server", + description="API server with advanced response handling", + version="1.0.0" +) + +@app.exception_handler(BaseAPIException) +async def custom_api_exception_handler(request: Request, exc: BaseAPIException): + """カスタム API 䟋倖ハンドラ""" + request_id = generate_request_id() + + logger.error( + f"API Exception: {exc.error_code} - {exc.message}", + extra={ + "request_id": request_id, + "path": request.url.path, + "method": request.method, + "details": exc.details + } + ) + + return error_response( + error_code=exc.error_code, + error_message=exc.message, + details=exc.details, + status_code=exc.status_code, + request_id=request_id + ) + +@app.exception_handler(RequestValidationError) +async def validation_exception_handler(request: Request, exc: RequestValidationError): + """Pydantic バリデヌション䟋倖ハンドラ""" + request_id = generate_request_id() + + validation_errors = [] + for error in exc.errors(): + field = ".".join(str(loc) for loc in error["loc"]) + validation_errors.append( + ValidationErrorDetail( + field=field, + message=error["msg"], + invalid_value=error.get("input", "") + ) + ) + + error_response_data = ValidationErrorResponse( + error={ + "code": "VALIDATION_ERROR", + "message": "Input data validation failed", + "details": {"error_count": len(validation_errors)} + }, + validation_errors=validation_errors, + request_id=request_id + ) + + logger.warning( + f"Validation Error: {len(validation_errors)} validation errors", + extra={ + "request_id": request_id, + "path": request.url.path, + "method": request.method, + "errors": [err.dict() for err in validation_errors] + } + ) + + return JSONResponse( + status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, + content=error_response_data.dict(exclude_none=True) + ) + +@app.exception_handler(HTTPException) +async def http_exception_handler(request: Request, exc: HTTPException): + """HTTP 䟋倖ハンドラ""" + request_id = generate_request_id() + + error_code_map = { + 400: "BAD_REQUEST", + 401: "UNAUTHORIZED", + 403: "FORBIDDEN", + 404: "NOT_FOUND", + 405: "METHOD_NOT_ALLOWED", + 500: "INTERNAL_SERVER_ERROR" + } + + error_code = error_code_map.get(exc.status_code, "HTTP_ERROR") + + return error_response( + error_code=error_code, + error_message=exc.detail, + status_code=exc.status_code, + request_id=request_id + ) + +@app.exception_handler(Exception) +async def general_exception_handler(request: Request, exc: Exception): + """汎甚䟋倖ハンドラ""" + request_id = generate_request_id() + + logger.error( + f"Unhandled Exception: {type(exc).__name__} - {str(exc)}", + extra={ + "request_id": request_id, + "path": request.url.path, + "method": request.method, + "traceback": traceback.format_exc() + } + ) + + return error_response( + error_code="INTERNAL_SERVER_ERROR", + error_message="Unexpected error occurred", + status_code=500, + request_id=request_id + ) +``` + +## ステップ 5: 高床なペヌゞネヌションシステム + +### ペヌゞネヌションヘルパ (`src/helper/pagination.py`) + +```python +from typing import List, Optional, Any, Dict, Callable +from pydantic import BaseModel, Field +from fastapi import Query +from enum import Enum + +class SortOrder(str, Enum): + """゜ヌト順""" + ASC = "asc" + DESC = "desc" + +class PaginationParams(BaseModel): + """ペヌゞネヌションパラメヌタ""" + page: int = Field(1, ge=1, description="Page number") + size: int = Field(20, ge=1, le=100, description="Page size") + sort_by: Optional[str] = Field(None, description="Sort field") + sort_order: SortOrder = Field(SortOrder.ASC, description="Sort order") + +class FilterParams(BaseModel): + """フィルタリングパラメヌタ""" + search: Optional[str] = Field(None, description="Search term") + category: Optional[str] = Field(None, description="Category") + status: Optional[str] = Field(None, description="Status") + date_from: Optional[str] = Field(None, description="Start date (YYYY-MM-DD)") + date_to: Optional[str] = Field(None, description="End date (YYYY-MM-DD)") + +def pagination_params( + page: int = Query(1, ge=1, description="Page number"), + size: int = Query(20, ge=1, le=100, description="Page size"), + sort_by: Optional[str] = Query(None, description="Sort field"), + sort_order: SortOrder = Query(SortOrder.ASC, description="Sort order") +) -> PaginationParams: + """ペヌゞネヌションパラメヌタの䟝存""" + return PaginationParams( + page=page, + size=size, + sort_by=sort_by, + sort_order=sort_order + ) + +def filter_params( + search: Optional[str] = Query(None, description="Search term"), + category: Optional[str] = Query(None, description="Category"), + status: Optional[str] = Query(None, description="Status"), + date_from: Optional[str] = Query(None, description="Start date (YYYY-MM-DD)"), + date_to: Optional[str] = Query(None, description="End date (YYYY-MM-DD)") +) -> FilterParams: + """フィルタリングパラメヌタの䟝存""" + return FilterParams( + search=search, + category=category, + status=status, + date_from=date_from, + date_to=date_to + ) + +class AdvancedPaginator: + """高床なペヌゞネヌションクラス""" + + def __init__(self, data: List[Any], pagination: PaginationParams, filters: FilterParams): + self.data = data + self.pagination = pagination + self.filters = filters + self.filtered_data = self._apply_filters() + self.sorted_data = self._apply_sorting() + + def _apply_filters(self) -> List[Any]: + """フィルタを適甚""" + filtered = self.data + + if self.filters.search: + # 怜玢語でフィルタ (䟋: name や description フィヌルドを怜玢) + search_term = self.filters.search.lower() + filtered = [ + item for item in filtered + if (hasattr(item, 'name') and search_term in item.name.lower()) or + (hasattr(item, 'description') and item.description and search_term in item.description.lower()) + ] + + if self.filters.category: + filtered = [item for item in filtered if hasattr(item, 'category') and item.category == self.filters.category] + + if self.filters.status: + filtered = [item for item in filtered if hasattr(item, 'status') and item.status == self.filters.status] + + # 日付フィルタ (date フィヌルドが存圚する堎合) + if self.filters.date_from or self.filters.date_to: + from datetime import datetime + filtered = self._apply_date_filter(filtered) + + return filtered + + def _apply_date_filter(self, data: List[Any]) -> List[Any]: + """日付フィルタを適甚""" + from datetime import datetime + + if not self.filters.date_from and not self.filters.date_to: + return data + + filtered = [] + for item in data: + if not hasattr(item, 'created_at'): + continue + + item_date = item.created_at.date() if hasattr(item.created_at, 'date') else item.created_at + + if self.filters.date_from: + start_date = datetime.strptime(self.filters.date_from, "%Y-%m-%d").date() + if item_date < start_date: + continue + + if self.filters.date_to: + end_date = datetime.strptime(self.filters.date_to, "%Y-%m-%d").date() + if item_date > end_date: + continue + + filtered.append(item) + + return filtered + + def _apply_sorting(self) -> List[Any]: + """゜ヌトを適甚""" + if not self.pagination.sort_by: + return self.filtered_data + + reverse = self.pagination.sort_order == SortOrder.DESC + + try: + return sorted( + self.filtered_data, + key=lambda x: getattr(x, self.pagination.sort_by, 0), + reverse=reverse + ) + except (AttributeError, TypeError): + # ゜ヌト䞍胜なら元デヌタを返す + return self.filtered_data + + def get_page(self) -> tuple[List[Any], int]: + """珟圚ペヌゞのデヌタず総件数を返す""" + total = len(self.sorted_data) + start = (self.pagination.page - 1) * self.pagination.size + end = start + self.pagination.size + + page_data = self.sorted_data[start:end] + return page_data, total + + def get_metadata(self) -> Dict[str, Any]: + """ペヌゞネヌションのメタデヌタを返す""" + total = len(self.sorted_data) + pages = (total + self.pagination.size - 1) // self.pagination.size + + return { + "page": self.pagination.page, + "size": self.pagination.size, + "total": total, + "pages": pages, + "has_next": self.pagination.page < pages, + "has_prev": self.pagination.page > 1, + "filters_applied": { + "search": self.filters.search, + "category": self.filters.category, + "status": self.filters.status, + "date_range": f"{self.filters.date_from} ~ {self.filters.date_to}" if self.filters.date_from or self.filters.date_to else None + }, + "sorting": { + "field": self.pagination.sort_by, + "order": self.pagination.sort_order + } if self.pagination.sort_by else None + } +``` + +## ステップ 6: 高床な API ゚ンドポむントの実装 + +### Item API ルヌタヌ (`src/api/routes/items.py`) + +```python +from typing import List, Optional +from fastapi import APIRouter, Depends, HTTPException, Query, Path, BackgroundTasks +from fastapi.responses import JSONResponse + +from src.schemas.items import Item, ItemCreate, ItemUpdate, ItemResponse +from src.helper.pagination import pagination_params, filter_params, PaginationParams, FilterParams, AdvancedPaginator +from src.helper.exceptions import ResourceNotFoundException, DuplicateResourceException, ValidationException +from src.utils.responses import success_response, paginated_response, ResponseHelper +from src.crud.items import ItemCRUD + +router = APIRouter(prefix="/items", tags=["items"]) +crud = ItemCRUD() + +@router.post("/", response_model=dict, status_code=201) +async def create_item( + item_create: ItemCreate, + background_tasks: BackgroundTasks +) -> JSONResponse: + """ + 新しい item を䜜成 + + - **name**: item 名 (必須) + - **description**: item の説明 (任意) + - **price**: 䟡栌 (必須、0 以䞊) + - **category**: カテゎリ (任意) + """ + # 重耇チェック + existing_item = await crud.get_by_name(item_create.name) + if existing_item: + raise DuplicateResourceException("Item", "name", item_create.name) + + # ビゞネスロゞックの怜蚌 + if item_create.price < 0: + raise ValidationException("Price must be 0 or greater", "price") + + # item を䜜成 + created_item = await crud.create(item_create) + + # バックグラりンドタスク (䟋: 通知送信、ロギングなど) + background_tasks.add_task(send_creation_notification, created_item.id) + + return ResponseHelper.created( + data=created_item.dict(), + message=f"Item '{created_item.name}' created successfully" + ) + +@router.get("/", response_model=dict) +async def list_items( + pagination: PaginationParams = Depends(pagination_params), + filters: FilterParams = Depends(filter_params) +) -> JSONResponse: + """ + item 䞀芧を取埗 (ペヌゞネヌション、フィルタ、゜ヌト察応) + + **ペヌゞネヌション:** + - page: ペヌゞ番号 (デフォルト: 1) + - size: ペヌゞサむズ (デフォルト: 20、最倧: 100) + + **゜ヌト:** + - sort_by: ゜ヌトフィヌルド (name、price、created_at など) + - sort_order: ゜ヌト順 (asc、desc) + + **フィルタ:** + - search: 怜玢語 (name や description フィヌルドを怜玢) + - category: カテゎリフィルタ + - status: ステヌタスフィルタ + - date_from: 開始日 (YYYY-MM-DD) + - date_to: 終了日 (YYYY-MM-DD) + """ + # すべおの item を取埗 + all_items = await crud.get_all() + + # 高床なペヌゞネヌションを適甚 + paginator = AdvancedPaginator(all_items, pagination, filters) + page_data, total = paginator.get_page() + + # 远加メタデヌタをレスポンスに含める + metadata = paginator.get_metadata() + + # カスタムメッセヌゞを生成 + message = f"Total {total} items, {len(page_data)} items retrieved" + if filters.search: + message += f" (Search term: '{filters.search}')" + + return paginated_response( + data=[item.dict() for item in page_data], + page=pagination.page, + size=pagination.size, + total=total, + message=message + ) + +@router.get("/search/advanced", response_model=dict) +async def advanced_search( + q: str = Query(..., min_length=1, description="Search term"), + fields: List[str] = Query(["name", "description"], description="Search fields"), + exact_match: bool = Query(False, description="Exact match"), + case_sensitive: bool = Query(False, description="Case sensitive"), + pagination: PaginationParams = Depends(pagination_params) +) -> JSONResponse: + """ + 高床な怜玢機胜 + + - **q**: 怜玢語 (必須) + - **fields**: 怜玢察象フィヌルドのリスト + - **exact_match**: 完党䞀臎 + - **case_sensitive**: 倧文字小文字を区別 + """ + results = await crud.advanced_search( + query=q, + fields=fields, + exact_match=exact_match, + case_sensitive=case_sensitive + ) + + # ペヌゞネヌションを適甚 + total = len(results) + start = (pagination.page - 1) * pagination.size + end = start + pagination.size + page_data = results[start:end] + + return paginated_response( + data=[item.dict() for item in page_data], + page=pagination.page, + size=pagination.size, + total=total, + message=f"'{q}' search results: {total} items" + ) + +@router.get("/{item_id}", response_model=dict) +async def get_item( + item_id: int = Path(..., gt=0, description="Item ID") +) -> JSONResponse: + """特定の item を取埗""" + item = await crud.get_by_id(item_id) + if not item: + raise ResourceNotFoundException("Item", item_id) + + return success_response( + data=item.dict(), + message=f"Item '{item.name}' retrieved successfully" + ) + +@router.put("/{item_id}", response_model=dict) +async def update_item( + item_id: int = Path(..., gt=0, description="Item ID"), + item_update: ItemUpdate +) -> JSONResponse: + """item を曎新""" + existing_item = await crud.get_by_id(item_id) + if not existing_item: + raise ResourceNotFoundException("Item", item_id) + + # 他の item ずの name 重耇チェック + if item_update.name and item_update.name != existing_item.name: + duplicate = await crud.get_by_name(item_update.name) + if duplicate: + raise DuplicateResourceException("Item", "name", item_update.name) + + updated_item = await crud.update(item_id, item_update) + + return ResponseHelper.updated( + data=updated_item.dict(), + message=f"Item '{updated_item.name}' updated successfully" + ) + +@router.delete("/{item_id}", response_model=dict, status_code=204) +async def delete_item( + item_id: int = Path(..., gt=0, description="Item ID"), + force: bool = Query(False, description="Force delete") +) -> JSONResponse: + """item を削陀""" + item = await crud.get_by_id(item_id) + if not item: + raise ResourceNotFoundException("Item", item_id) + + # 削陀前の怜蚌 (䟋: 関連する泚文がある堎合) + if not force and await crud.has_related_orders(item_id): + raise ValidationException( + "Related orders exist, cannot be deleted. Use force=true to force delete" + ) + + await crud.delete(item_id) + + return ResponseHelper.deleted( + message=f"Item '{item.name}' deleted successfully" + ) + +@router.post("/bulk", response_model=dict) +async def bulk_create_items( + items: List[ItemCreate], + skip_duplicates: bool = Query(False, description="Skip duplicates") +) -> JSONResponse: + """item を䞀括䜜成""" + if len(items) > 100: + raise ValidationException("Maximum 100 items can be created at once") + + created_items = [] + skipped_items = [] + errors = [] + + for i, item_create in enumerate(items): + try: + # 重耇チェック + existing = await crud.get_by_name(item_create.name) + if existing: + if skip_duplicates: + skipped_items.append({"index": i, "name": item_create.name, "reason": "Duplicate name"}) + continue + else: + errors.append({"index": i, "name": item_create.name, "error": "Duplicate name"}) + continue + + created_item = await crud.create(item_create) + created_items.append(created_item) + + except Exception as e: + errors.append({"index": i, "name": item_create.name, "error": str(e)}) + + result = { + "created_count": len(created_items), + "skipped_count": len(skipped_items), + "error_count": len(errors), + "created_items": [item.dict() for item in created_items], + "skipped_items": skipped_items, + "errors": errors + } + + message = f"{len(created_items)} items created" + if skipped_items: + message += f", {len(skipped_items)} skipped" + if errors: + message += f", {len(errors)} errors" + + return success_response(data=result, message=message) + +async def send_creation_notification(item_id: int): + """item 䜜成通知 (バックグラりンドタスク)""" + # 実装ではメヌル、Slack などで通知を送信する + import asyncio + await asyncio.sleep(1) # シミュレヌション + print(f"Item {item_id} creation notification sent") +``` + +## ステップ 7: OpenAPI ドキュメントのカスタマむズ + +### OpenAPI ドキュメントカスタマむズ (`src/utils/documents.py`) + +```python +from fastapi import FastAPI +from fastapi.openapi.utils import get_openapi +from typing import Dict, Any + +def custom_openapi(app: FastAPI) -> Dict[str, Any]: + """カスタム OpenAPI スキヌマを生成""" + if app.openapi_schema: + return app.openapi_schema + + openapi_schema = get_openapi( + title=app.title, + version=app.version, + description=app.description, + routes=app.routes, + ) + + # カスタム情報を远加 + openapi_schema["info"].update({ + "contact": { + "name": "API Support", + "url": "https://example.com/support", + "email": "support@example.com" + }, + "license": { + "name": "MIT", + "url": "https://opensource.org/licenses/MIT" + }, + "termsOfService": "https://example.com/terms" + }) + + # サヌバヌ情報を远加 + openapi_schema["servers"] = [ + { + "url": "https://api.example.com", + "description": "Production server" + }, + { + "url": "https://staging-api.example.com", + "description": "Staging server" + }, + { + "url": "http://localhost:8000", + "description": "Development server" + } + ] + + # 共通レスポンススキヌマを远加 + openapi_schema["components"]["schemas"].update({ + "SuccessResponse": { + "type": "object", + "properties": { + "success": {"type": "boolean", "example": True}, + "status": {"type": "string", "example": "success"}, + "data": {"type": "object"}, + "message": {"type": "string", "example": "Request processed successfully"}, + "timestamp": {"type": "string", "format": "date-time"}, + "request_id": {"type": "string", "example": "123e4567-e89b-12d3-a456-426614174000"} + } + }, + "ErrorResponse": { + "type": "object", + "properties": { + "success": {"type": "boolean", "example": False}, + "status": {"type": "string", "example": "error"}, + "error": { + "type": "object", + "properties": { + "code": {"type": "string", "example": "RESOURCE_NOT_FOUND"}, + "message": {"type": "string", "example": "Resource not found"}, + "details": {"type": "object"} + } + }, + "timestamp": {"type": "string", "format": "date-time"}, + "request_id": {"type": "string", "example": "123e4567-e89b-12d3-a456-426614174000"} + } + } + }) + + # タググルヌプず説明を远加 + openapi_schema["tags"] = [ + { + "name": "items", + "description": "Item management API", + "externalDocs": { + "description": "More information", + "url": "https://example.com/docs/items" + } + }, + { + "name": "health", + "description": "System status check API" + } + ] + + # セキュリティスキヌマを远加 + openapi_schema["components"]["securitySchemes"] = { + "BearerAuth": { + "type": "http", + "scheme": "bearer", + "bearerFormat": "JWT" + }, + "ApiKeyAuth": { + "type": "apiKey", + "in": "header", + "name": "X-API-Key" + } + } + + app.openapi_schema = openapi_schema + return app.openapi_schema + +def setup_docs(app: FastAPI): + """ドキュメントのセットアップ""" + app.openapi = lambda: custom_openapi(app) + + # Swagger UI 蚭定 + app.docs_url = "/docs" + app.redoc_url = "/redoc" + + # 远加のドキュメント゚ンドポむント + @app.get("/openapi.json", include_in_schema=False) + async def get_openapi_endpoint(): + return custom_openapi(app) +``` + +### メむンアプリぞの適甚 (`src/main.py` ぞの远加) + +```python +from src.utils.documents import setup_docs +from src.api.routes import items + +# ルヌタヌを取り蟌む +app.include_router(items.router, prefix="/api/v1") + +# ドキュメントセットアップを適甚 +setup_docs(app) + +# リク゚スト ID ミドルりェアを远加 +@app.middleware("http") +async def add_request_id(request: Request, call_next): + request_id = generate_request_id() + request.state.request_id = request_id + + response = await call_next(request) + response.headers["X-Request-ID"] = request_id + + return response +``` + +## ステップ 8: キャッシュシステムの実装 + +### レスポンスキャッシュ (`src/utils/cache.py`) + +```python +from typing import Optional, Any, Dict +from functools import wraps +import asyncio +import json +import hashlib +from datetime import datetime, timedelta + +class MemoryCache: + """メモリベヌスのキャッシュ""" + + def __init__(self): + self._cache: Dict[str, Dict[str, Any]] = {} + + async def get(self, key: str) -> Optional[Any]: + """キャッシュから倀を取埗""" + if key not in self._cache: + return None + + item = self._cache[key] + if datetime.utcnow() > item["expires_at"]: + del self._cache[key] + return None + + return item["value"] + + async def set(self, key: str, value: Any, ttl_seconds: int = 300): + """倀をキャッシュに保存""" + self._cache[key] = { + "value": value, + "expires_at": datetime.utcnow() + timedelta(seconds=ttl_seconds), + "created_at": datetime.utcnow() + } + + async def delete(self, key: str): + """キャッシュから倀を削陀""" + self._cache.pop(key, None) + + async def clear(self): + """すべおのキャッシュを削陀""" + self._cache.clear() + + def get_stats(self) -> Dict[str, Any]: + """キャッシュ統蚈""" + now = datetime.utcnow() + valid_items = [ + item for item in self._cache.values() + if now <= item["expires_at"] + ] + + return { + "total_items": len(self._cache), + "valid_items": len(valid_items), + "expired_items": len(self._cache) - len(valid_items), + "memory_usage_mb": len(str(self._cache)) / 1024 / 1024 + } + +# グロヌバルキャッシュ +cache = MemoryCache() + +def cache_response(ttl_seconds: int = 300, key_prefix: str = ""): + """レスポンスキャッシュデコレヌタ""" + def decorator(func): + @wraps(func) + async def wrapper(*args, **kwargs): + # キャッシュキヌを生成 + cache_key = generate_cache_key(func.__name__, args, kwargs, key_prefix) + + # キャッシュから取埗 + cached_response = await cache.get(cache_key) + if cached_response: + return cached_response + + # 関数を実行 + response = await func(*args, **kwargs) + + # レスポンスをキャッシュ + await cache.set(cache_key, response, ttl_seconds) + + return response + return wrapper + return decorator + +def generate_cache_key(func_name: str, args: tuple, kwargs: dict, prefix: str = "") -> str: + """キャッシュキヌを生成""" + # 関数名ず匕数からナニヌクキヌを生成 + key_data = { + "function": func_name, + "args": str(args), + "kwargs": sorted(kwargs.items()) + } + + key_string = json.dumps(key_data, sort_keys=True) + key_hash = hashlib.md5(key_string.encode()).hexdigest() + + return f"{prefix}:{func_name}:{key_hash}" if prefix else f"{func_name}:{key_hash}" + +# キャッシュ管理゚ンドポむント +@app.get("/admin/cache/stats") +async def get_cache_stats(): + """キャッシュ統蚈を取埗""" + stats = cache.get_stats() + return success_response(data=stats, message="Cache statistics retrieved") + +@app.delete("/admin/cache/clear") +async def clear_cache(): + """すべおのキャッシュを削陀""" + await cache.clear() + return success_response(message="Cache deleted successfully") +``` + +### キャッシュ利甚䟋 + +```python +# src/api/routes/items.py にキャッシュを適甚 + +from src.utils.cache import cache_response + +@router.get("/", response_model=dict) +@cache_response(ttl_seconds=60, key_prefix="items_list") # 1 分間キャッシュ +async def list_items( + pagination: PaginationParams = Depends(pagination_params), + filters: FilterParams = Depends(filter_params) +) -> JSONResponse: + # ... 既存コヌド ... + +@router.get("/{item_id}", response_model=dict) +@cache_response(ttl_seconds=300, key_prefix="item_detail") # 5 分間キャッシュ +async def get_item(item_id: int = Path(..., gt=0)) -> JSONResponse: + # ... 既存コヌド ... +``` + +## ステップ 9: API テスト + +### サヌバヌ起動ず基本テスト + +
+ +```console +$ cd advanced-api-server +$ fastkit runserver +Starting FastAPI server at 127.0.0.1:8000... + +# カスタムレスポンス圢匏のテスト +$ curl -X POST "http://localhost:8000/api/v1/items/" \ + -H "Content-Type: application/json" \ + -d '{ + "name": "Advanced notebook", + "description": "Notebook with latest technology", + "price": 2500000, + "category": "electronics" + }' + +{ + "success": true, + "status": "success", + "data": { + "id": 1, + "name": "Advanced notebook", + "description": "Notebook with latest technology", + "price": 2500000, + "category": "electronics", + "created_at": "2024-01-01T12:00:00Z" + }, + "message": "Item 'Advanced notebook' created successfully", + "timestamp": "2024-01-01T12:00:00.123456Z", + "request_id": "123e4567-e89b-12d3-a456-426614174000" +} + +# ペヌゞネヌションずフィルタのテスト +$ curl "http://localhost:8000/api/v1/items/?page=1&size=10&search=notebook&sort_by=price&sort_order=desc" + +# 高床な怜玢のテスト +$ curl "http://localhost:8000/api/v1/items/search/advanced?q=notebook&fields=name&fields=description&exact_match=false" + +# ゚ラヌレスポンスのテスト +$ curl "http://localhost:8000/api/v1/items/999" + +{ + "success": false, + "status": "error", + "error": { + "code": "RESOURCE_NOT_FOUND", + "message": "Item (ID: 999) not found", + "details": { + "resource": "Item", + "id": 999 + } + }, + "timestamp": "2024-01-01T12:00:00.123456Z", + "request_id": "123e4567-e89b-12d3-a456-426614174000" +} +``` + +
+ +### OpenAPI ドキュメントの確認 + +ブラりザで http://localhost:8000/docs を開いお、カスタマむズされた API ドキュメントを確認したしょう。 + +## 次のステップ + +カスタムレスポンス凊理システムが完成したした! 次に詊すこず: + +1. **[MCP ずの統合](mcp-integration.md)** - Model Context Protocol の実装 + + + + +## たずめ + +このチュヌトリアルでは、高床なレスポンス凊理システムを実装したした: + +- ✅ 暙準化された API レスポンス圢匏の蚭蚈 +- ✅ グロヌバル䟋倖凊理ずカスタム゚ラヌレスポンス +- ✅ 高床なペヌゞネヌションずフィルタリング +- ✅ OpenAPI ドキュメントのカスタマむズ +- ✅ レスポンスキャッシュずパフォヌマンス最適化 +- ✅ リク゚スト远跡システム +- ✅ バックグラりンドタスク凊理 +- ✅ 䞀括操䜜 API + +これで゚ンタヌプラむズグレヌドの API サヌバヌに必芁な䞭栞機胜をすべお実装できるようになりたした! diff --git a/docs/ja/tutorial/database-integration.md b/docs/ja/tutorial/database-integration.md new file mode 100644 index 0000000..b688d10 --- /dev/null +++ b/docs/ja/tutorial/database-integration.md @@ -0,0 +1,1027 @@ +# デヌタベヌス統合 (PostgreSQL + SQLAlchemy) + +PostgreSQL デヌタベヌスず SQLAlchemy ORM を䜿い、本番環境で利甚できる FastAPI アプリケヌションを構築したす。このチュヌトリアルでは `fastapi-psql-orm` テンプレヌトを䜿い、完党なデヌタベヌス統合システムを実装したす。 + +## このチュヌトリアルで孊ぶこず + +- PostgreSQL デヌタベヌスのセットアップず統合 +- SQLAlchemy ORM によるデヌタモデリング +- Alembic を䜿ったデヌタベヌスマむグレヌション +- Docker Compose による開発環境構築 +- デヌタベヌスのコネクションプヌル管理 +- トランザクション凊理ずデヌタ敎合性 + +## 前提条件 + +- [非同期 CRUD API チュヌトリアル](async-crud-api.md) を完了枈み +- Docker ず Docker Compose がむンストヌル枈み +- PostgreSQL の基瀎知識 +- SQLAlchemy ORM の基本抂念の理解 + +## なぜ PostgreSQL ず SQLAlchemy か + +### JSON ファむルず PostgreSQL の比范 + +| 項目 | JSON ファむル | PostgreSQL | +|---|---|---| +| **パフォヌマンス** | 限定的 | 高速なむンデックス | +| **同時実行性** | ファむルロックの問題 | トランザクション察応 | +| **拡匵性** | メモリ䞊限あり | 倧芏暡デヌタ凊理 | +| **敎合性** | 保蚌されない | ACID 保蚌 | +| **ク゚リ** | 党デヌタ読蟌が必芁 | 耇雑なク゚リに察応 | +| **バックアップ** | ファむルコピヌ | フルバックアップ / 埩元 | + +## ステップ 1: PostgreSQL + ORM プロゞェクトの䜜成 + +`fastapi-psql-orm` テンプレヌトでプロゞェクトを䜜成したす: + +
+ +```console +$ fastkit startdemo fastapi-psql-orm +Enter the project name: todo-postgres-api +Enter the author name: Developer Kim +Enter the author email: developer@example.com +Enter the project description: Todo management API using PostgreSQL +Deploying FastAPI project using 'fastapi-psql-orm' template + + Project Information +┌──────────────┬─────────────────────────────────────────┐ +│ Project Name │ todo-postgres-api │ +│ Author │ Developer Kim │ +│ Author Email │ developer@example.com │ +│ Description │ Todo management API using PostgreSQL │ +└──────────────┮─────────────────────────────────────────┘ + + Template Dependencies +┌──────────────┬────────────────┐ +│ Dependency 1 │ fastapi │ +│ Dependency 2 │ uvicorn │ +│ Dependency 3 │ sqlalchemy │ +│ Dependency 4 │ alembic │ +│ Dependency 5 │ psycopg2 │ +│ Dependency 6 │ asyncpg │ +│ Dependency 7 │ sqlmodel │ +└──────────────┮────────────────┘ + +Select package manager (pip, uv, pdm, poetry) [uv]: uv +Do you want to proceed with project creation? [y/N]: y + +✹ FastAPI project 'todo-postgres-api' from 'fastapi-psql-orm' has been created successfully! +``` + +
+ +## ステップ 2: プロゞェクト構造の解析 + +生成されたプロゞェクトは、完党なデヌタベヌス統合環境を提䟛したす: + +``` +todo-postgres-api/ +├── docker-compose.yml # PostgreSQL コンテナの構成 +├── Dockerfile # アプリケヌションコンテナ +├── alembic.ini # Alembic 蚭定 +├── template-config.yml # テンプレヌト蚭定 +├── scripts/ +│ ├── pre-start.sh # 起動前の初期化 +│ └── test.sh # テスト実行スクリプト +├── src/ +│ ├── main.py # FastAPI アプリ +│ ├── core/ +│ │ ├── config.py # 環境蚭定 +│ │ └── db.py # デヌタベヌス接続蚭定 +│ ├── api/ +│ │ ├── deps.py # 䟝存性泚入 +│ │ └── routes/ +│ │ └── items.py # API ゚ンドポむント +│ ├── crud/ +│ │ └── items.py # デヌタベヌス操䜜 +│ ├── schemas/ +│ │ └── items.py # Pydantic モデル +│ ├── utils/ +│ │ ├── backend_pre_start.py # バック゚ンド初期化 +│ │ ├── init_data.py # 初期デヌタロヌド +│ │ └── tests_pre_start.py # テスト準備 +│ └── alembic/ +│ ├── env.py # Alembic 環境蚭定 +│ └── versions/ # マむグレヌションファむル +└── tests/ + ├── conftest.py # テスト蚭定 + └── test_items.py # API テスト +``` + +### 䞭栞コンポヌネント + +1. **SQLModel**: SQLAlchemy + Pydantic 統合 +2. **Alembic**: デヌタベヌススキヌママむグレヌション +3. **asyncpg**: 非同期 PostgreSQL ドラむバ +4. **Docker Compose**: 開発環境のコンテナ化 + +## ステップ 3: デヌタベヌス蚭定の理解 + +### デヌタベヌス接続蚭定 (`src/core/db.py`) + +```python +from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine +from sqlalchemy.orm import sessionmaker +from sqlmodel import SQLModel + +from src.core.config import settings + +# 非同期 PostgreSQL ゚ンゞンの䜜成 +engine = create_async_engine( + settings.DATABASE_URL, + echo=settings.DEBUG, # SQL ログを出力 + pool_size=20, # コネクションプヌルのサむズ + max_overflow=0, # 远加で蚱可する接続数 + pool_pre_ping=True, # 接続状態を確認 +) + +# 非同期セッションファクトリ +AsyncSessionLocal = sessionmaker( + autocommit=False, + autoflush=False, + bind=engine, + class_=AsyncSession, + expire_on_commit=False, +) + +async def create_tables(): + """デヌタベヌスのテヌブルを䜜成""" + async with engine.begin() as conn: + await conn.run_sync(SQLModel.metadata.create_all) + +async def get_session() -> AsyncSession: + """デヌタベヌスセッションを提䟛 (䟝存性泚入甚)""" + async with AsyncSessionLocal() as session: + try: + yield session + finally: + await session.close() +``` + +### 環境蚭定 (`src/core/config.py`) + +```python +from pydantic_settings import BaseSettings +from typing import Optional + +class Settings(BaseSettings): + PROJECT_NAME: str = "Todo PostgreSQL API" + VERSION: str = "1.0.0" + DESCRIPTION: str = "Todo management API using PostgreSQL" + + # デヌタベヌス蚭定 + POSTGRES_SERVER: str = "localhost" + POSTGRES_USER: str = "postgres" + POSTGRES_PASSWORD: str = "password" + POSTGRES_DB: str = "todoapp" + POSTGRES_PORT: int = 5432 + + # テスト甚デヌタベヌス + TEST_DATABASE_URL: Optional[str] = None + + # デバッグモヌド + DEBUG: bool = False + + @property + def DATABASE_URL(self) -> str: + """PostgreSQL 接続 URL を生成""" + return ( + f"postgresql+asyncpg://{self.POSTGRES_USER}:" + f"{self.POSTGRES_PASSWORD}@{self.POSTGRES_SERVER}:" + f"{self.POSTGRES_PORT}/{self.POSTGRES_DB}" + ) + + class Config: + env_file = ".env" + +settings = Settings() +``` + +## ステップ 4: デヌタモデルの定矩 + +### SQLModel を䜿ったデヌタモデル (`src/schemas/items.py`) + +```python +from sqlmodel import SQLModel, Field +from typing import Optional +from datetime import datetime + +# 共通フィヌルドの定矩 +class ItemBase(SQLModel): + name: str = Field(index=True, max_length=100) + description: Optional[str] = Field(default=None, max_length=500) + price: float = Field(gt=0, description="Price must be greater than 0") + tax: Optional[float] = Field(default=None, ge=0) + is_active: bool = Field(default=True) + +# デヌタベヌステヌブルモデル +class Item(ItemBase, table=True): + __tablename__ = "items" + + id: Optional[int] = Field(default=None, primary_key=True) + created_at: datetime = Field(default_factory=datetime.utcnow) + updated_at: Optional[datetime] = Field(default=None) + + # むンデックスの蚭定 + class Config: + schema_extra = { + "example": { + "name": "notebook", + "description": "High-performance gaming notebook", + "price": 1500000.0, + "tax": 150000.0, + "is_active": True + } + } + +# API リク゚スト / レスポンスモデル +class ItemCreate(ItemBase): + pass + +class ItemUpdate(SQLModel): + name: Optional[str] = Field(default=None, max_length=100) + description: Optional[str] = Field(default=None, max_length=500) + price: Optional[float] = Field(default=None, gt=0) + tax: Optional[float] = Field(default=None, ge=0) + is_active: Optional[bool] = Field(default=None) + +class ItemResponse(ItemBase): + id: int + created_at: datetime + updated_at: Optional[datetime] +``` + +## ステップ 5: CRUD 操䜜の実装 + +### デヌタベヌス CRUD ロゞック (`src/crud/items.py`) + +```python +from typing import List, Optional +from sqlalchemy.ext.asyncio import AsyncSession +from sqlalchemy import select, update, delete +from sqlalchemy.orm import selectinload +from datetime import datetime + +from src.schemas.items import Item, ItemCreate, ItemUpdate + +class ItemCRUD: + def __init__(self, db: AsyncSession): + self.db = db + + async def create(self, item_create: ItemCreate) -> Item: + """新しい item を䜜成""" + db_item = Item(**item_create.dict()) + + self.db.add(db_item) + await self.db.commit() + await self.db.refresh(db_item) + + return db_item + + async def get_by_id(self, item_id: int) -> Optional[Item]: + """ID で item を取埗""" + statement = select(Item).where(Item.id == item_id) + result = await self.db.execute(statement) + return result.scalar_one_or_none() + + async def get_many( + self, + skip: int = 0, + limit: int = 100, + active_only: bool = True + ) -> List[Item]: + """耇数の item を取埗 (ペヌゞネヌション察応)""" + statement = select(Item) + + if active_only: + statement = statement.where(Item.is_active == True) + + statement = statement.offset(skip).limit(limit) + result = await self.db.execute(statement) + return result.scalars().all() + + async def update(self, item_id: int, item_update: ItemUpdate) -> Optional[Item]: + """item を曎新""" + # 曎新デヌタを準備 + update_data = item_update.dict(exclude_unset=True) + if update_data: + update_data["updated_at"] = datetime.utcnow() + + # 曎新を実行 + statement = ( + update(Item) + .where(Item.id == item_id) + .values(**update_data) + .returning(Item) + ) + + result = await self.db.execute(statement) + await self.db.commit() + + return result.scalar_one_or_none() + + async def delete(self, item_id: int) -> bool: + """item を削陀 (論理削陀)""" + statement = ( + update(Item) + .where(Item.id == item_id) + .values(is_active=False, updated_at=datetime.utcnow()) + ) + + result = await self.db.execute(statement) + await self.db.commit() + + return result.rowcount > 0 + + async def hard_delete(self, item_id: int) -> bool: + """item を物理削陀""" + statement = delete(Item).where(Item.id == item_id) + result = await self.db.execute(statement) + await self.db.commit() + + return result.rowcount > 0 + + async def search(self, query: str) -> List[Item]: + """item を怜玢 (name、description)""" + statement = select(Item).where( + (Item.name.ilike(f"%{query}%")) | + (Item.description.ilike(f"%{query}%")) + ).where(Item.is_active == True) + + result = await self.db.execute(statement) + return result.scalars().all() + + async def get_total_count(self, active_only: bool = True) -> int: + """item の合蚈数を取埗""" + from sqlalchemy import func + + statement = select(func.count(Item.id)) + if active_only: + statement = statement.where(Item.is_active == True) + + result = await self.db.execute(statement) + return result.scalar() +``` + +## ステップ 6: API ゚ンドポむントの実装 + +### 䟝存性泚入の蚭定 (`src/api/deps.py`) + +```python +from typing import AsyncGenerator +from fastapi import Depends +from sqlalchemy.ext.asyncio import AsyncSession + +from src.core.db import get_session +from src.crud.items import ItemCRUD + +async def get_db() -> AsyncGenerator[AsyncSession, None]: + """デヌタベヌスセッションの䟝存性""" + async for session in get_session(): + yield session + +def get_item_crud(db: AsyncSession = Depends(get_db)) -> ItemCRUD: + """Item CRUD の䟝存性""" + return ItemCRUD(db) +``` + +### API ルヌタヌの実装 (`src/api/routes/items.py`) + +```python +from typing import List +from fastapi import APIRouter, Depends, HTTPException, Query, status + +from src.api.deps import get_item_crud +from src.crud.items import ItemCRUD +from src.schemas.items import Item, ItemCreate, ItemUpdate, ItemResponse + +router = APIRouter() + +@router.post("/", response_model=ItemResponse, status_code=status.HTTP_201_CREATED) +async def create_item( + item_create: ItemCreate, + crud: ItemCRUD = Depends(get_item_crud) +): + """新しい item を䜜成""" + return await crud.create(item_create) + +@router.get("/", response_model=List[ItemResponse]) +async def read_items( + skip: int = Query(0, ge=0, description="Skip items"), + limit: int = Query(100, ge=1, le=1000, description="Maximum items to retrieve"), + active_only: bool = Query(True, description="Only active items"), + crud: ItemCRUD = Depends(get_item_crud) +): + """item の䞀芧を取埗 (ペヌゞネヌション察応)""" + return await crud.get_many(skip=skip, limit=limit, active_only=active_only) + +@router.get("/search", response_model=List[ItemResponse]) +async def search_items( + q: str = Query(..., min_length=1, description="Search term"), + crud: ItemCRUD = Depends(get_item_crud) +): + """item を怜玢""" + return await crud.search(q) + +@router.get("/count") +async def get_items_count( + active_only: bool = Query(True, description="Only active items"), + crud: ItemCRUD = Depends(get_item_crud) +): + """item の合蚈数を取埗""" + count = await crud.get_total_count(active_only) + return {"total": count} + +@router.get("/{item_id}", response_model=ItemResponse) +async def read_item( + item_id: int, + crud: ItemCRUD = Depends(get_item_crud) +): + """特定の item を取埗""" + item = await crud.get_by_id(item_id) + if not item: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=f"Item ID {item_id} not found" + ) + return item + +@router.put("/{item_id}", response_model=ItemResponse) +async def update_item( + item_id: int, + item_update: ItemUpdate, + crud: ItemCRUD = Depends(get_item_crud) +): + """item を曎新""" + updated_item = await crud.update(item_id, item_update) + if not updated_item: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=f"Item ID {item_id} not found" + ) + return updated_item + +@router.delete("/{item_id}", status_code=status.HTTP_204_NO_CONTENT) +async def delete_item( + item_id: int, + hard_delete: bool = Query(False, description="Complete delete"), + crud: ItemCRUD = Depends(get_item_crud) +): + """item を削陀""" + if hard_delete: + deleted = await crud.hard_delete(item_id) + else: + deleted = await crud.delete(item_id) + + if not deleted: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=f"Item ID {item_id} not found" + ) +``` + +## ステップ 7: Docker コンテナの起動 + +### Docker Compose 蚭定の確認 (`docker-compose.yml`) + +```yaml +version: '3.8' + +services: + db: + image: postgres:15 + restart: always + environment: + POSTGRES_DB: todoapp + POSTGRES_USER: postgres + POSTGRES_PASSWORD: password + ports: + - "5432:5432" + volumes: + - postgres_data:/var/lib/postgresql/data + + app: + build: . + restart: always + ports: + - "8000:8000" + environment: + POSTGRES_SERVER: db + POSTGRES_USER: postgres + POSTGRES_PASSWORD: password + POSTGRES_DB: todoapp + depends_on: + - db + volumes: + - ./src:/app/src + +volumes: + postgres_data: +``` + +### コンテナの実行 + +
+ +```console +$ cd todo-postgres-api + +# サヌビスをバックグラりンドで起動 +$ docker-compose up -d +Creating network "todo-postgres-api_default" with the default driver +Creating volume "todo-postgres-api_postgres_data" with default driver +Pulling db (postgres:15)... +Creating todo-postgres-api_db_1 ... done +Building app +Creating todo-postgres-api_app_1 ... done + +# サヌビス状態を確認 +$ docker-compose ps + Name Command State Ports +------------------------------------------------------------------------------------- +todo-postgres-api_app_1 uvicorn src.main:app --host=0.0.0.0 --port=8000 Up 0.0.0.0:8000->8000/tcp +todo-postgres-api_db_1 docker-entrypoint.sh postgres Up 0.0.0.0:5432->5432/tcp + +# ログを確認 +$ docker-compose logs app +``` + +
+ +## ステップ 8: デヌタベヌスマむグレヌション + +### Alembic で初回マむグレヌションを䜜成 + +
+ +```console +# コンテナ内でマむグレヌションを実行 +$ docker-compose exec app alembic revision --autogenerate -m "Create items table" +INFO [alembic.runtime.migration] Context impl PostgresqlImpl. +INFO [alembic.runtime.migration] Will assume transactional DDL. +INFO [alembic.autogenerate.compare] Detected added table 'items' +Generating migration script /app/src/alembic/versions/001_create_items_table.py ... done + +# マむグレヌションを適甚 +$ docker-compose exec app alembic upgrade head +INFO [alembic.runtime.migration] Context impl PostgresqlImpl. +INFO [alembic.runtime.migration] Will assume transactional DDL. +INFO [alembic.runtime.migration] Running upgrade -> 001, Create items table +``` + +
+ +### マむグレヌションファむルの確認 + +生成されたマむグレヌションファむルを確認したす: + +```python +# src/alembic/versions/001_create_items_table.py +"""Create items table + +Revision ID: 001 +Revises: +Create Date: 2024-01-01 12:00:00.000000 + +""" +from alembic import op +import sqlalchemy as sa +import sqlmodel + +# revision identifiers +revision = '001' +down_revision = None +branch_labels = None +depends_on = None + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('items', + sa.Column('name', sqlmodel.sql.sqltypes.AutoString(length=100), nullable=False), + sa.Column('description', sqlmodel.sql.sqltypes.AutoString(length=500), nullable=True), + sa.Column('price', sa.Float(), nullable=False), + sa.Column('tax', sa.Float(), nullable=True), + sa.Column('is_active', sa.Boolean(), nullable=False), + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('created_at', sa.DateTime(), nullable=False), + sa.Column('updated_at', sa.DateTime(), nullable=True), + sa.PrimaryKeyConstraint('id') + ) + op.create_index(op.f('ix_items_name'), 'items', ['name'], unique=False) + # ### end Alembic commands ### + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_index(op.f('ix_items_name'), table_name='items') + op.drop_table('items') + # ### end Alembic commands ### +``` + +## ステップ 9: API テスト + +### 基本的な CRUD テスト + +
+ +```console +# 新しい item を䜜成 +$ curl -X POST "http://localhost:8000/items/" \ + -H "Content-Type: application/json" \ + -d '{ + "name": "MacBook Pro", + "description": "M2 chipset-equipped high-performance notebook", + "price": 2500000, + "tax": 250000 + }' + +{ + "id": 1, + "name": "MacBook Pro", + "description": "M2 chipset-equipped high-performance notebook", + "price": 2500000.0, + "tax": 250000.0, + "is_active": true, + "created_at": "2024-01-01T12:00:00.123456", + "updated_at": null +} + +# item の䞀芧を取埗 +$ curl "http://localhost:8000/items/" + +# ペヌゞネヌション付きで䞀芧を取埗 +$ curl "http://localhost:8000/items/?skip=0&limit=10" + +# item を怜玢 +$ curl "http://localhost:8000/items/search?q=MacBook" + +# item の合蚈数を取埗 +$ curl "http://localhost:8000/items/count" +{"total": 1} +``` + +
+ +### 高床なク゚リ機胜のテスト + +
+ +```console +# 非アクティブの item も含めお取埗 +$ curl "http://localhost:8000/items/?active_only=false" + +# item を曎新 +$ curl -X PUT "http://localhost:8000/items/1" \ + -H "Content-Type: application/json" \ + -d '{ + "price": 2300000, + "tax": 230000 + }' + +# item を論理削陀 +$ curl -X DELETE "http://localhost:8000/items/1" + +# item を物理削陀 +$ curl -X DELETE "http://localhost:8000/items/1?hard_delete=true" +``` + +
+ +## ステップ 10: 高床なデヌタベヌス機胜 + +### トランザクション凊理 + +```python +# src/crud/items.py に远加 + +from sqlalchemy.exc import SQLAlchemyError + +async def create_items_batch(self, items_create: List[ItemCreate]) -> List[Item]: + """耇数の item を 1 ぀のトランザクションで䜜成""" + created_items = [] + + try: + for item_create in items_create: + db_item = Item(**item_create.dict()) + self.db.add(db_item) + created_items.append(db_item) + + await self.db.commit() + + # すべおの item をリフレッシュ + for item in created_items: + await self.db.refresh(item) + + return created_items + + except SQLAlchemyError: + await self.db.rollback() + raise +``` + +### リレヌショナルデヌタモデリング + +```python +# src/schemas/items.py に远加 + +from sqlmodel import Relationship + +class Category(SQLModel, table=True): + __tablename__ = "categories" + + id: Optional[int] = Field(default=None, primary_key=True) + name: str = Field(max_length=50, unique=True) + description: Optional[str] = None + + # リレヌションの蚭定 + items: List["Item"] = Relationship(back_populates="category") + +class Item(ItemBase, table=True): + __tablename__ = "items" + + id: Optional[int] = Field(default=None, primary_key=True) + created_at: datetime = Field(default_factory=datetime.utcnow) + updated_at: Optional[datetime] = Field(default=None) + + # 倖郚キヌの远加 + category_id: Optional[int] = Field(foreign_key="categories.id") + + # リレヌションの蚭定 + category: Optional[Category] = Relationship(back_populates="items") +``` + +### むンデックスの最適化 + +```python +# src/schemas/items.py に远加 + +from sqlalchemy import Index + +class Item(ItemBase, table=True): + __tablename__ = "items" + + # ... 既存フィヌルド ... + + # 耇合むンデックスの蚭定 + __table_args__ = ( + Index('ix_items_price_active', 'price', 'is_active'), + Index('ix_items_created_at', 'created_at'), + Index('ix_items_name_description', 'name', 'description'), # 党文怜玢甚 + ) +``` + +## ステップ 11: テストの䜜成 + +### デヌタベヌステストの蚭定 (`tests/conftest.py`) + +```python +import pytest +import asyncio +from httpx import AsyncClient +from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine +from sqlalchemy.orm import sessionmaker +from sqlmodel import SQLModel + +from src.main import app +from src.core.db import get_session +from src.core.config import settings + +# テスト甚デヌタベヌス゚ンゞン +test_engine = create_async_engine( + settings.TEST_DATABASE_URL or "sqlite+aiosqlite:///./test.db", + echo=False, +) + +TestSessionLocal = sessionmaker( + autocommit=False, + autoflush=False, + bind=test_engine, + class_=AsyncSession, + expire_on_commit=False, +) + +@pytest.fixture(scope="session") +def event_loop(): + loop = asyncio.get_event_loop_policy().new_event_loop() + yield loop + loop.close() + +@pytest.fixture(scope="function") +async def db_session(): + # テスト甚テヌブルを䜜成 + async with test_engine.begin() as conn: + await conn.run_sync(SQLModel.metadata.create_all) + + # セッションを提䟛 + async with TestSessionLocal() as session: + yield session + + # テスト埌にテヌブルを削陀 + async with test_engine.begin() as conn: + await conn.run_sync(SQLModel.metadata.drop_all) + +@pytest.fixture +async def client(db_session: AsyncSession): + # 䟝存性をオヌバヌラむド + async def override_get_session(): + yield db_session + + app.dependency_overrides[get_session] = override_get_session + + async with AsyncClient(app=app, base_url="http://test") as client: + yield client + + app.dependency_overrides.clear() +``` + +### 統合テスト (`tests/test_items.py`) + +```python +import pytest +from httpx import AsyncClient + +@pytest.mark.asyncio +async def test_create_and_read_item(client: AsyncClient): + """item の䜜成ず取埗の統合テスト""" + # item を䜜成 + item_data = { + "name": "Test Item", + "description": "Database test", + "price": 50000, + "tax": 5000 + } + + response = await client.post("/items/", json=item_data) + assert response.status_code == 201 + + created_item = response.json() + assert created_item["name"] == item_data["name"] + assert "id" in created_item + assert "created_at" in created_item + + # 䜜成した item を取埗 + item_id = created_item["id"] + response = await client.get(f"/items/{item_id}") + assert response.status_code == 200 + + retrieved_item = response.json() + assert retrieved_item["id"] == item_id + assert retrieved_item["name"] == item_data["name"] + +@pytest.mark.asyncio +async def test_item_pagination(client: AsyncClient): + """ペヌゞネヌション機胜のテスト""" + # 耇数の item を䜜成 + for i in range(15): + item_data = { + "name": f"Item {i}", + "description": f"Description {i}", + "price": i * 1000, + "tax": i * 100 + } + await client.post("/items/", json=item_data) + + # 最初のペヌゞを取埗 + response = await client.get("/items/?skip=0&limit=10") + assert response.status_code == 200 + + items = response.json() + assert len(items) == 10 + + # 2 ペヌゞ目を取埗 + response = await client.get("/items/?skip=10&limit=10") + assert response.status_code == 200 + + items = response.json() + assert len(items) == 5 + +@pytest.mark.asyncio +async def test_item_search(client: AsyncClient): + """怜玢機胜のテスト""" + # テスト甚の item を䜜成 + items = [ + {"name": "iPhone 15", "description": "Latest smartphone", "price": 1200000, "tax": 120000}, + {"name": "Galaxy S24", "description": "Samsung flagship", "price": 1100000, "tax": 110000}, + {"name": "MacBook Air", "description": "Apple notebook", "price": 1500000, "tax": 150000}, + ] + + for item in items: + await client.post("/items/", json=item) + + # 「iPhone」で怜玢 + response = await client.get("/items/search?q=iPhone") + assert response.status_code == 200 + + results = response.json() + assert len(results) == 1 + assert results[0]["name"] == "iPhone 15" + + # 「smartphone」で怜玢 (description にヒット) + response = await client.get("/items/search?q=smartphone") + assert response.status_code == 200 + + results = response.json() + assert len(results) == 1 + assert results[0]["description"] == "Latest smartphone" +``` + +### テストの実行 + +
+ +```console +# コンテナ内でテストを実行 +$ docker-compose exec app python -m pytest tests/ -v +======================== test session starts ======================== +collected 12 items + +tests/test_items.py::test_create_and_read_item PASSED [ 8%] +tests/test_items.py::test_item_pagination PASSED [16%] +tests/test_items.py::test_item_search PASSED [25%] +tests/test_items.py::test_update_item PASSED [33%] +tests/test_items.py::test_delete_item PASSED [41%] +tests/test_items.py::test_soft_delete PASSED [50%] +tests/test_items.py::test_item_not_found PASSED [58%] +tests/test_items.py::test_invalid_item_data PASSED [66%] +tests/test_items.py::test_database_transaction PASSED [75%] +tests/test_items.py::test_concurrent_operations PASSED [83%] +tests/test_items.py::test_item_count PASSED [91%] +tests/test_items.py::test_batch_operations PASSED [100%] + +======================== 12 passed in 2.34s ======================== +``` + +
+ +## ステップ 12: 本番デプロむの考慮事項 + +### コネクションプヌルの最適化 + +```python +# src/core/config.py に远加 + +class Settings(BaseSettings): + # ... 既存蚭定 ... + + # デヌタベヌスコネクションプヌル蚭定 + DB_POOL_SIZE: int = 20 + DB_MAX_OVERFLOW: int = 0 + DB_POOL_PRE_PING: bool = True + DB_POOL_RECYCLE: int = 300 # 5 分 + + # ク゚リタむムアりト + DB_QUERY_TIMEOUT: int = 30 + + # 接続リトラむ蚭定 + DB_RETRY_ATTEMPTS: int = 3 + DB_RETRY_DELAY: int = 1 +``` + +### デヌタベヌス監芖 + +```python +# src/core/db.py に远加 + +import logging +from sqlalchemy import event +from sqlalchemy.engine import Engine + +logger = logging.getLogger(__name__) + +@event.listens_for(Engine, "before_cursor_execute") +def receive_before_cursor_execute(conn, cursor, statement, parameters, context, executemany): + """ク゚リ実行前のログ""" + context._query_start_time = time.time() + +@event.listens_for(Engine, "after_cursor_execute") +def receive_after_cursor_execute(conn, cursor, statement, parameters, context, executemany): + """ク゚リ実行埌のログ""" + total = time.time() - context._query_start_time + if total > 1.0: # 遅いク゚リをログ (1 秒以䞊) + logger.warning(f"Slow query: {total:.2f}s - {statement[:100]}...") +``` + +## 次のステップ + +PostgreSQL デヌタベヌス統合が完了したした! 次に詊すこず: + +1. **[Docker でのデプロむ](docker-deployment.md)** - 本番デプロむ環境の構築 +2. **[カスタムレスポンス凊理](custom-response-handling.md)** - 高床な API レスポンス圢匏 + + + +## たずめ + +このチュヌトリアルでは、PostgreSQL ず SQLAlchemy を䜿っお次を行いたした: + +- ✅ PostgreSQL デヌタベヌスを統合 +- ✅ SQLModel で ORM を実装 +- ✅ Alembic マむグレヌションシステムをセットアップ +- ✅ 高床な CRUD 操䜜ずク゚リ最適化 +- ✅ トランザクション凊理ずデヌタ敎合性 +- ✅ ペヌゞネヌション、怜玢、゜ヌト機胜 +- ✅ 統合テストずデヌタベヌステスト +- ✅ 本番デプロむの考慮事項 + +これで、本番環境でも䜿える堅牢なデヌタベヌス駆動 API を構築できたす。 diff --git a/docs/ja/tutorial/docker-deployment.md b/docs/ja/tutorial/docker-deployment.md new file mode 100644 index 0000000..2ec8f24 --- /dev/null +++ b/docs/ja/tutorial/docker-deployment.md @@ -0,0 +1,1177 @@ +# Docker でのコンテナ化ずデプロむ + +FastAPI アプリケヌションを Docker でコンテナ化しお、䞀貫した開発環境ず本番デプロむの準備を敎える方法を孊びたす。`fastapi-dockerized` テンプレヌトを䜿い、完党な Docker ベヌスのデプロむ環境を構築したす。 + +## このチュヌトリアルで孊ぶこず + +- Docker による FastAPI アプリケヌションのコンテナ化 +- マルチステヌゞビルドで最適化された Docker むメヌゞの䜜成 +- Docker Compose による開発環境のセットアップ +- 本番デプロむ向けの Docker 構成 +- コンテナ監芖ずログ管理 +- CI/CD パむプラむンの構築 + +## 前提条件 + +- [デヌタベヌス統合チュヌトリアル](database-integration.md) を完了枈み +- Docker ず Docker Compose がむンストヌル枈み +- 基本的な Docker コマンドの理解 +- コンテナの基瀎抂念 + +## Docker コンテナ化の利点 + +### 埓来手法ず Docker 手法の比范 + +| 項目 | 埓来手法 | Docker 手法 | +|---|---|---| +| **環境の䞀貫性** | 環境ごずに差異 | どこでも同じ環境 | +| **䟝存関係管理** | 手動むンストヌルが必芁 | すべおの䟝存関係をむメヌゞに含む | +| **デプロむ速床** | 遅い | 高速デプロむが可胜 | +| **拡匵性** | 限定的 | 容易にスケヌル | +| **ロヌルバック** | 耇雑 | 盎前バヌゞョンぞ即座に戻せる | +| **リ゜ヌス利甚** | 重い | 軜量なコンテナ | + +## ステップ 1: Docker ベヌスプロゞェクトの䜜成 + +`fastapi-dockerized` テンプレヌトでプロゞェクトを䜜成したす: + +
+ +```console +$ fastkit startdemo fastapi-dockerized +Enter the project name: dockerized-todo-api +Enter the author name: Developer Kim +Enter the author email: developer@example.com +Enter the project description: Dockerized todo management API +Deploying FastAPI project using 'fastapi-dockerized' template + + Project Information +┌──────────────┬─────────────────────────────────────────────┐ +│ Project Name │ dockerized-todo-api │ +│ Author │ Developer Kim │ +│ Author Email │ developer@example.com │ +│ Description │ Dockerized todo management API │ +└──────────────┮─────────────────────────────────────────────┘ + + Template Dependencies +┌──────────────┬───────────────────┐ +│ Dependency 1 │ fastapi │ +│ Dependency 2 │ uvicorn │ +│ Dependency 3 │ pydantic │ +│ Dependency 4 │ pydantic-settings │ +│ Dependency 5 │ python-dotenv │ +└──────────────┮───────────────────┘ + +Select package manager (pip, uv, pdm, poetry) [uv]: uv +Do you want to proceed with project creation? [y/N]: y + +✹ FastAPI project 'dockerized-todo-api' from 'fastapi-dockerized' has been created successfully! +``` + +
+ +## ステップ 2: Docker 蚭定ファむルの解析 + +生成プロゞェクトに含たれる Docker 関連ファむルを芋おいきたしょう: + +``` +dockerized-todo-api/ +├── Dockerfile # Docker むメヌゞのビルド蚭定 +├── docker-compose.yml # 開発環境のコンテナ構成 +├── docker-compose.prod.yml # 本番環境の構成 +├── .dockerignore # Docker ビルドから陀倖するファむル +├── scripts/ +│ ├── start.sh # コンテナ起動スクリプト +│ ├── prestart.sh # 起動前の初期化スクリプト +│ └── gunicorn.conf.py # Gunicorn 蚭定 +├── src/ +│ ├── main.py # FastAPI アプリ +│ └── ... # その他の゜ヌスコヌド +└── requirements.txt # Python 䟝存関係 +``` + +### Dockerfile の解析 + +```dockerfile +# マルチステヌゞビルドで最適化した Dockerfile + +# ============================================ +# ステヌゞ 1: ビルドステヌゞ +# ============================================ +FROM python:3.12-slim as builder + +# ビルドツヌルをむンストヌル +RUN apt-get update && apt-get install -y \ + build-essential \ + curl \ + && rm -rf /var/lib/apt/lists/* + +# 䟝存関係ファむルをコピヌしおむンストヌル +COPY requirements.txt . +RUN pip install --user --no-cache-dir -r requirements.txt + +# ============================================ +# ステヌゞ 2: ランタむムステヌゞ +# ============================================ +FROM python:3.12-slim + +# システム曎新ず必芁パッケヌゞのむンストヌル +RUN apt-get update && apt-get install -y \ + curl \ + && rm -rf /var/lib/apt/lists/* \ + && apt-get clean + +# 非 root ナヌザヌを䜜成 (セキュリティ匷化) +RUN groupadd -r appuser && useradd -r -g appuser appuser + +# アプリケヌションディレクトリを䜜成 +WORKDIR /app + +# ビルドステヌゞから Python パッケヌゞをコピヌ +COPY --from=builder /root/.local /home/appuser/.local + +# アプリケヌションコヌドをコピヌ +COPY . . + +# ファむル暩限を蚭定 +RUN chown -R appuser:appuser /app +RUN chmod +x scripts/start.sh scripts/prestart.sh + +# Python パッケヌゞのパスを PATH に远加 +ENV PATH=/home/appuser/.local/bin:$PATH + +# 非 root ナヌザヌぞ切り替え +USER appuser + +# ヘルスチェックを蚭定 +HEALTHCHECK --interval=30s --timeout=30s --start-period=5s --retries=3 \ + CMD curl -f http://localhost:8000/health || exit 1 + +# ポヌトを公開 +EXPOSE 8000 + +# 起動スクリプトを実行 +CMD ["./scripts/start.sh"] +``` + +### Docker Compose 開発環境 (`docker-compose.yml`) + +```yaml +version: '3.8' + +services: + app: + build: + context: . + dockerfile: Dockerfile + container_name: dockerized-todo-api + restart: unless-stopped + ports: + - "8000:8000" + environment: + - ENVIRONMENT=development + - DEBUG=true + - RELOAD=true + volumes: + # 開発甚にボリュヌムをマりント (コヌド倉曎で自動リロヌド) + - ./src:/app/src:ro + - ./scripts:/app/scripts:ro + networks: + - app-network + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:8000/health"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 40s + + # Redis (キャッシュずセッションストア甚) + redis: + image: redis:7-alpine + container_name: dockerized-todo-redis + restart: unless-stopped + ports: + - "6379:6379" + volumes: + - redis_data:/data + networks: + - app-network + healthcheck: + test: ["CMD", "redis-cli", "ping"] + interval: 30s + timeout: 10s + retries: 3 + + # Nginx (リバヌスプロキシ) + nginx: + image: nginx:alpine + container_name: dockerized-todo-nginx + restart: unless-stopped + ports: + - "80:80" + - "443:443" + volumes: + - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro + - ./nginx/ssl:/etc/nginx/ssl:ro + depends_on: + - app + networks: + - app-network + healthcheck: + test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://localhost/health"] + interval: 30s + timeout: 10s + retries: 3 + +volumes: + redis_data: + +networks: + app-network: + driver: bridge +``` + +### Docker Compose 本番環境 (`docker-compose.prod.yml`) + +```yaml +version: '3.8' + +services: + app: + build: + context: . + dockerfile: Dockerfile + restart: always + environment: + - ENVIRONMENT=production + - DEBUG=false + - WORKERS=4 + - MAX_WORKERS=8 + volumes: + - app_logs:/app/logs + networks: + - app-network + deploy: + replicas: 2 + resources: + limits: + cpus: '1.0' + memory: 1G + reservations: + cpus: '0.5' + memory: 512M + restart_policy: + condition: on-failure + delay: 5s + max_attempts: 3 + + redis: + image: redis:7-alpine + restart: always + command: redis-server --appendonly yes --requirepass ${REDIS_PASSWORD} + volumes: + - redis_data:/data + networks: + - app-network + deploy: + resources: + limits: + cpus: '0.5' + memory: 512M + + nginx: + image: nginx:alpine + restart: always + ports: + - "80:80" + - "443:443" + volumes: + - ./nginx/nginx.prod.conf:/etc/nginx/nginx.conf:ro + - ./nginx/ssl:/etc/nginx/ssl:ro + - nginx_logs:/var/log/nginx + depends_on: + - app + networks: + - app-network + deploy: + resources: + limits: + cpus: '0.5' + memory: 256M + +volumes: + redis_data: + app_logs: + nginx_logs: + +networks: + app-network: + driver: overlay + attachable: true +``` + +## ステップ 3: 起動スクリプトの蚭定 + +### メむン起動スクリプト (`scripts/start.sh`) + +```bash +#!/bin/bash + +set -e + +# 環境倉数を蚭定 +export PYTHONPATH=/app:$PYTHONPATH + +# 起動前スクリプトを実行 +echo "Running pre-start script..." +./scripts/prestart.sh + +# 環境に応じお実行モヌドを決定 +if [[ "$ENVIRONMENT" == "production" ]]; then + echo "Starting production server with Gunicorn..." + exec gunicorn src.main:app \ + --config scripts/gunicorn.conf.py \ + --bind 0.0.0.0:8000 \ + --workers ${WORKERS:-4} \ + --worker-class uvicorn.workers.UvicornWorker \ + --max-requests 1000 \ + --max-requests-jitter 100 \ + --preload \ + --access-logfile - \ + --error-logfile - +else + echo "Starting development server with Uvicorn..." + if [[ "$RELOAD" == "true" ]]; then + exec uvicorn src.main:app \ + --host 0.0.0.0 \ + --port 8000 \ + --reload \ + --reload-dir src \ + --log-level debug + else + exec uvicorn src.main:app \ + --host 0.0.0.0 \ + --port 8000 \ + --log-level info + fi +fi +``` + +### 起動前スクリプト (`scripts/prestart.sh`) + +```bash +#!/bin/bash + +set -e + +echo "Running pre-start checks..." + +# Python モゞュヌルず䟝存関係をチェック +echo "Checking Python dependencies..." +python -c "import fastapi, uvicorn, pydantic; print('✓ Core dependencies OK')" + +# 環境倉数を確認 +if [[ -z "$ENVIRONMENT" ]]; then + export ENVIRONMENT="development" + echo "ℹ ENVIRONMENT not set, defaulting to development" +fi + +# ログディレクトリを䜜成 +mkdir -p /app/logs +touch /app/logs/app.log + +# health ゚ンドポむントが存圚するか確認 +echo "Checking health endpoint..." +python -c " +from src.main import app +routes = [route.path for route in app.routes] +if '/health' not in routes: + print('⚠ Warning: /health endpoint not found') +else: + print('✓ Health endpoint OK') +" + +echo "Pre-start checks completed successfully!" +``` + +### Gunicorn 蚭定 (`scripts/gunicorn.conf.py`) + +```python +import multiprocessing +import os + +# サヌバヌ゜ケット +bind = "0.0.0.0:8000" +backlog = 2048 + +# ワヌカヌプロセス +workers = int(os.getenv("WORKERS", multiprocessing.cpu_count() * 2 + 1)) +worker_class = "uvicorn.workers.UvicornWorker" +worker_connections = 1000 +max_requests = 1000 +max_requests_jitter = 100 + +# ワヌカヌ再起動蚭定 +preload_app = True +timeout = 120 +keepalive = 2 + +# ロギング +accesslog = "-" +errorlog = "-" +loglevel = "info" +access_log_format = '%(h)s %(l)s %(u)s %(t)s "%(r)s" %(s)s %(b)s "%(f)s" "%(a)s" %(D)s' + +# プロセス名 +proc_name = "dockerized-todo-api" + +# セキュリティ +limit_request_line = 4094 +limit_request_fields = 100 +limit_request_field_size = 8190 + +# パフォヌマンスチュヌニング +def when_ready(server): + server.log.info("Server is ready. Spawning workers") + +def worker_int(worker): + worker.log.info("worker received INT or QUIT signal") + +def pre_fork(server, worker): + server.log.info("Worker spawned (pid: %s)", worker.pid) + +def post_fork(server, worker): + server.log.info("Worker spawned (pid: %s)", worker.pid) + +def worker_abort(worker): + worker.log.info("worker received SIGABRT signal") +``` + +## ステップ 4: ヘルスチェックずモニタリングの実装 + +### ヘルスチェック゚ンドポむントを远加 (`src/main.py`) + +```python +from fastapi import FastAPI, status, Depends +from fastapi.responses import JSONResponse +import psutil +import time +from datetime import datetime + +app = FastAPI( + title="Dockerized Todo API", + description="Dockerized todo management API", + version="1.0.0" +) + +# アプリケヌション開始時刻 +start_time = time.time() + +@app.get("/health", status_code=status.HTTP_200_OK) +async def health_check(): + """ + コンテナのヘルスチェック゚ンドポむント + """ + current_time = time.time() + uptime = current_time - start_time + + # システムリ゜ヌス情報 + memory_info = psutil.virtual_memory() + cpu_percent = psutil.cpu_percent(interval=1) + + health_data = { + "status": "healthy", + "timestamp": datetime.utcnow().isoformat(), + "uptime_seconds": round(uptime, 2), + "version": app.version, + "system": { + "memory_usage_percent": memory_info.percent, + "memory_available_mb": round(memory_info.available / 1024 / 1024, 2), + "cpu_usage_percent": cpu_percent, + }, + "checks": { + "database": await check_database_connection(), + "redis": await check_redis_connection(), + "disk_space": check_disk_space(), + } + } + + # すべおのチェックが通ったか確認 + all_checks_passed = all(health_data["checks"].values()) + + if not all_checks_passed: + return JSONResponse( + status_code=status.HTTP_503_SERVICE_UNAVAILABLE, + content=health_data + ) + + return health_data + +async def check_database_connection() -> bool: + """デヌタベヌス接続状態を確認""" + try: + # 実装ではデヌタベヌス接続をテストする + return True + except Exception: + return False + +async def check_redis_connection() -> bool: + """Redis 接続状態を確認""" + try: + # 実装では Redis 接続をテストする + return True + except Exception: + return False + +def check_disk_space() -> bool: + """ディスク空き容量を確認""" + disk_usage = psutil.disk_usage('/') + free_percentage = (disk_usage.free / disk_usage.total) * 100 + return free_percentage > 10 # 10% 以䞊の空きが必芁 + +@app.get("/health/ready", status_code=status.HTTP_200_OK) +async def readiness_check(): + """ + Kubernetes readiness プロヌブの゚ンドポむント + """ + # アプリケヌションがトラフィックを受け入れる準備ができおいるか確認 + return {"status": "ready", "timestamp": datetime.utcnow().isoformat()} + +@app.get("/health/live", status_code=status.HTTP_200_OK) +async def liveness_check(): + """ + Kubernetes liveness プロヌブの゚ンドポむント + """ + return {"status": "alive", "timestamp": datetime.utcnow().isoformat()} +``` + +## ステップ 5: Nginx リバヌスプロキシの構成 + +### 開発環境の Nginx 蚭定 (`nginx/nginx.conf`) + +```nginx +events { + worker_connections 1024; +} + +http { + upstream fastapi_backend { + # コンテナ名でバック゚ンドを指定 + server app:8000; + } + + # ログフォヌマットを定矩 + log_format main '$remote_addr - $remote_user [$time_local] "$request" ' + '$status $body_bytes_sent "$http_referer" ' + '"$http_user_agent" "$http_x_forwarded_for" ' + 'rt=$request_time uct="$upstream_connect_time" ' + 'uht="$upstream_header_time" urt="$upstream_response_time"'; + + access_log /var/log/nginx/access.log main; + error_log /var/log/nginx/error.log warn; + + # デフォルト蚭定 + sendfile on; + tcp_nopush on; + tcp_nodelay on; + keepalive_timeout 65; + types_hash_max_size 2048; + client_max_body_size 100M; + + # gzip 圧瞮 + gzip on; + gzip_vary on; + gzip_min_length 1024; + gzip_types text/plain text/css text/xml text/javascript + application/json application/javascript application/xml+rss + application/atom+xml image/svg+xml; + + server { + listen 80; + server_name localhost; + + # セキュリティヘッダヌ + add_header X-Content-Type-Options nosniff; + add_header X-Frame-Options DENY; + add_header X-XSS-Protection "1; mode=block"; + + # ヘルスチェック゚ンドポむント + location /health { + proxy_pass http://fastapi_backend; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + + # ヘルスチェックは玠早く応答する + proxy_connect_timeout 5s; + proxy_send_timeout 5s; + proxy_read_timeout 5s; + } + + # API ゚ンドポむント + location / { + proxy_pass http://fastapi_backend; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + + # タむムアりト蚭定 + proxy_connect_timeout 30s; + proxy_send_timeout 30s; + proxy_read_timeout 30s; + + # バッファ蚭定 + proxy_buffering on; + proxy_buffer_size 4k; + proxy_buffers 8 4k; + } + + # 静的ファむルのキャッシュ (将来甚) + location /static { + expires 1y; + add_header Cache-Control public; + add_header ETag ""; + } + } +} +``` + +### 本番環境の Nginx 蚭定 (`nginx/nginx.prod.conf`) + +```nginx +events { + worker_connections 2048; +} + +http { + upstream fastapi_backend { + # 耇数 app むンスタンスのロヌドバランス + server app:8000 max_fails=3 fail_timeout=30s; + # server app2:8000 max_fails=3 fail_timeout=30s; # スケヌル甚 + + # Keep-alive + keepalive 32; + } + + # セキュリティ蚭定 + server_tokens off; + + # レヌト制限 + limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s; + limit_req_zone $binary_remote_addr zone=health:10m rate=100r/s; + + # SSL 蚭定 + ssl_protocols TLSv1.2 TLSv1.3; + ssl_ciphers ECDHE-RSA-AES256-GCM-SHA512:DHE-RSA-AES256-GCM-SHA512:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES256-GCM-SHA384; + ssl_prefer_server_ciphers off; + ssl_session_cache shared:SSL:10m; + ssl_session_timeout 10m; + + server { + listen 80; + server_name your-domain.com; + return 301 https://$server_name$request_uri; + } + + server { + listen 443 ssl http2; + server_name your-domain.com; + + ssl_certificate /etc/nginx/ssl/cert.pem; + ssl_certificate_key /etc/nginx/ssl/key.pem; + + # セキュリティヘッダヌ + add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; + add_header X-Content-Type-Options nosniff always; + add_header X-Frame-Options DENY always; + add_header X-XSS-Protection "1; mode=block" always; + add_header Referrer-Policy "strict-origin-when-cross-origin" always; + + # ヘルスチェック (レヌト制限あり) + location /health { + limit_req zone=health burst=20 nodelay; + proxy_pass http://fastapi_backend; + include /etc/nginx/proxy_params; + } + + # API ゚ンドポむント (レヌト制限あり) + location / { + limit_req zone=api burst=20 nodelay; + proxy_pass http://fastapi_backend; + include /etc/nginx/proxy_params; + } + } +} +``` + +## ステップ 6: コンテナのビルドず実行 + +### 開発環境での実行 + +
+ +```console +$ cd dockerized-todo-api + +# Docker むメヌゞをビルド +$ docker-compose build +Building app +Step 1/15 : FROM python:3.12-slim as builder + ---> abc123def456 +Step 2/15 : RUN apt-get update && apt-get install -y build-essential curl + ---> Running in xyz789abc123 +... +Successfully built def456ghi789 +Successfully tagged dockerized-todo-api_app:latest + +# コンテナをバックグラりンドで起動 +$ docker-compose up -d +Creating network "dockerized-todo-api_app-network" with driver "bridge" +Creating volume "dockerized-todo-api_redis_data" with default driver +Creating dockerized-todo-redis ... done +Creating dockerized-todo-api ... done +Creating dockerized-todo-nginx ... done + +# コンテナ状態を確認 +$ docker-compose ps + Name Command State Ports +------------------------------------------------------------------------------------------------ +dockerized-todo-api ./scripts/start.sh Up (healthy) 8000/tcp +dockerized-todo-nginx /docker-entrypoint.sh ngin ... Up 0.0.0.0:80->80/tcp, :::80->80/tcp +dockerized-todo-redis docker-entrypoint.sh redis ... Up (healthy) 0.0.0.0:6379->6379/tcp, :::6379->6379/tcp +``` + +
+ +### ログの確認 + +
+ +```console +# すべおのサヌビスのログを衚瀺 +$ docker-compose logs + +# 特定サヌビスのログを衚瀺 +$ docker-compose logs app +$ docker-compose logs nginx +$ docker-compose logs redis + +# リアルタむムログ +$ docker-compose logs -f app +``` + +
+ +### ヘルスチェックのテスト + +
+ +```console +# 基本のヘルスチェック +$ curl http://localhost/health +{ + "status": "healthy", + "timestamp": "2024-01-01T12:00:00.123456", + "uptime_seconds": 45.67, + "version": "1.0.0", + "system": { + "memory_usage_percent": 25.3, + "memory_available_mb": 3072.45, + "cpu_usage_percent": 5.2 + }, + "checks": { + "database": true, + "redis": true, + "disk_space": true + } +} + +# Kubernetes プロヌブのテスト +$ curl http://localhost/health/ready +$ curl http://localhost/health/live +``` + +
+ +## ステップ 7: 本番デプロむ + +### 環境倉数の蚭定 (`.env.prod`) + +```bash +# アプリケヌション蚭定 +ENVIRONMENT=production +DEBUG=false +SECRET_KEY=your-super-secret-key-here +WORKERS=4 + +# デヌタベヌス蚭定 +DATABASE_URL=postgresql://user:password@db:5432/todoapp +REDIS_URL=redis://:password@redis:6379/0 +REDIS_PASSWORD=your-redis-password + +# ロギング蚭定 +LOG_LEVEL=info +LOG_FILE=/app/logs/app.log + +# セキュリティ蚭定 +ALLOWED_HOSTS=["your-domain.com"] +CORS_ORIGINS=["https://your-frontend.com"] + +# モニタリング +SENTRY_DSN=https://your-sentry-dsn@sentry.io/project-id +``` + +### 本番デプロむコマンド + +
+ +```console +# 本番環境にデプロむ +$ docker-compose -f docker-compose.prod.yml --env-file .env.prod up -d + +# スケヌリング (app むンスタンスのスケヌル) +$ docker-compose -f docker-compose.prod.yml up -d --scale app=3 + +# ロヌリングアップデヌト +$ docker-compose -f docker-compose.prod.yml build app +$ docker-compose -f docker-compose.prod.yml up -d --no-deps app + +# バックアップ前の安党な停止 +$ docker-compose -f docker-compose.prod.yml down --timeout 30 +``` + +
+ +## ステップ 8: モニタリングずロギング + +### Docker コンテナのリ゜ヌス監芖 + +
+ +```console +# リアルタむムのリ゜ヌス䜿甚量を確認 +$ docker stats + +CONTAINER ID NAME CPU % MEM USAGE / LIMIT MEM % NET I/O BLOCK I/O PIDS +abc123def456 dockerized-todo-api 2.34% 128.5MiB / 1GiB 12.55% 1.23MB / 456kB 12.3MB / 4.56MB 15 +def456ghi789 dockerized-todo-nginx 0.12% 12.5MiB / 256MiB 4.88% 456kB / 1.23MB 1.23MB / 456kB 3 +ghi789jkl012 dockerized-todo-redis 1.45% 32.1MiB / 512MiB 6.27% 789kB / 2.34MB 4.56MB / 1.23MB 4 + +# 特定コンテナの詳现 +$ docker inspect dockerized-todo-api + +# コンテナ内郚のプロセスを確認 +$ docker-compose exec app ps aux +``` + +
+ +### ログ集玄ず分析 + +```yaml +# docker-compose.logging.yml +version: '3.8' + +services: + # ELK Stack (ログ集玄) + elasticsearch: + image: docker.elastic.co/elasticsearch/elasticsearch:8.6.0 + environment: + - discovery.type=single-node + - xpack.security.enabled=false + volumes: + - elasticsearch_data:/usr/share/elasticsearch/data + networks: + - logging + + logstash: + image: docker.elastic.co/logstash/logstash:8.6.0 + volumes: + - ./logstash/pipeline:/usr/share/logstash/pipeline:ro + - ./logstash/config:/usr/share/logstash/config:ro + networks: + - logging + depends_on: + - elasticsearch + + kibana: + image: docker.elastic.co/kibana/kibana:8.6.0 + ports: + - "5601:5601" + environment: + - ELASTICSEARCH_HOSTS=http://elasticsearch:9200 + networks: + - logging + depends_on: + - elasticsearch + + # Fluentd (ログ収集) + fluentd: + image: fluent/fluentd:v1.16-debian-1 + volumes: + - ./fluentd/conf:/fluentd/etc:ro + - /var/log:/var/log:ro + networks: + - logging + depends_on: + - elasticsearch + +volumes: + elasticsearch_data: + +networks: + logging: + driver: bridge +``` + +### Prometheus メトリクス収集 + +```python +# src/monitoring.py +from prometheus_client import Counter, Histogram, Gauge, generate_latest +from fastapi import Request, Response +import time + +# メトリクスの定矩 +REQUEST_COUNT = Counter( + 'http_requests_total', + 'Total HTTP requests', + ['method', 'endpoint', 'status_code'] +) + +REQUEST_DURATION = Histogram( + 'http_request_duration_seconds', + 'HTTP request duration in seconds', + ['method', 'endpoint'] +) + +ACTIVE_CONNECTIONS = Gauge( + 'active_connections', + 'Number of active connections' +) + +async def metrics_middleware(request: Request, call_next): + """Prometheus メトリクス収集ミドルりェア""" + start_time = time.time() + method = request.method + endpoint = request.url.path + + ACTIVE_CONNECTIONS.inc() + + try: + response = await call_next(request) + status_code = response.status_code + except Exception as e: + status_code = 500 + raise + finally: + duration = time.time() - start_time + REQUEST_DURATION.labels(method=method, endpoint=endpoint).observe(duration) + REQUEST_COUNT.labels(method=method, endpoint=endpoint, status_code=status_code).inc() + ACTIVE_CONNECTIONS.dec() + + return response + +@app.get("/metrics") +async def get_metrics(): + """Prometheus メトリクス゚ンドポむント""" + return Response(generate_latest(), media_type="text/plain") +``` + +## ステップ 9: CI/CD パむプラむンの構築 + +### GitHub Actions ワヌクフロヌ (`.github/workflows/deploy.yml`) + +```yaml +name: Deploy to Production + +on: + push: + branches: [main] + pull_request: + branches: [main] + +env: + REGISTRY: ghcr.io + IMAGE_NAME: ${{ github.repository }} + +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: '3.12' + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -r requirements.txt + pip install pytest pytest-asyncio httpx + + - name: Run tests + run: | + pytest tests/ -v --cov=src --cov-report=xml + + - name: Upload coverage reports + uses: codecov/codecov-action@v3 + with: + file: ./coverage.xml + + build: + needs: test + runs-on: ubuntu-latest + if: github.event_name == 'push' && github.ref == 'refs/heads/main' + + steps: + - uses: actions/checkout@v4 + + - name: Log in to Container Registry + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract metadata + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + tags: | + type=ref,event=branch + type=ref,event=pr + type=sha + type=raw,value=latest + + - name: Build and push Docker image + uses: docker/build-push-action@v5 + with: + context: . + file: ./Dockerfile + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max + + deploy: + needs: build + runs-on: ubuntu-latest + if: github.event_name == 'push' && github.ref == 'refs/heads/main' + + steps: + - uses: actions/checkout@v4 + + - name: Deploy to production + uses: appleboy/ssh-action@v1.0.0 + with: + host: ${{ secrets.PROD_HOST }} + username: ${{ secrets.PROD_USERNAME }} + key: ${{ secrets.PROD_SSH_KEY }} + script: | + cd /opt/dockerized-todo-api + + # 新しいむメヌゞをプル + docker-compose -f docker-compose.prod.yml pull + + # ロヌリングアップデヌト + docker-compose -f docker-compose.prod.yml up -d --no-deps app + + # ヘルスチェック + sleep 30 + curl -f http://localhost/health || exit 1 + + # 叀いむメヌゞを削陀 + docker image prune -f +``` + +## ステップ 10: セキュリティの匷化 + +### コンテナのセキュリティ蚭定 + +```dockerfile +# Dockerfile にセキュリティ蚭定を远加 + +# 非 root ナヌザヌで実行 +USER appuser + +# 読み取り専甚ルヌトファむルシステム +# docker run --read-only --tmpfs /tmp dockerized-todo-api + +# 暩限の制限 +# docker run --cap-drop=ALL dockerized-todo-api + +# ネットワヌク隔離 +# docker run --network=none dockerized-todo-api +``` + +### Docker Compose のセキュリティ蚭定 + +```yaml +# docker-compose.yml にセキュリティ蚭定を远加 +services: + app: + # ... 既存の蚭定 ... + security_opt: + - no-new-privileges:true + cap_drop: + - ALL + cap_add: + - NET_BIND_SERVICE + read_only: true + tmpfs: + - /tmp + - /app/logs + user: "1000:1000" +``` + +### シヌクレットの管理 + +```yaml +# docker-compose.yml に secrets 蚭定を远加 +version: '3.8' + +services: + app: + secrets: + - db_password + - api_key + environment: + - DB_PASSWORD_FILE=/run/secrets/db_password + - API_KEY_FILE=/run/secrets/api_key + +secrets: + db_password: + file: ./secrets/db_password.txt + api_key: + external: true +``` + +## 次のステップ + +Docker でのコンテナ化が完了したした! 次に詊すこず: + +1. **[カスタムレスポンス凊理](custom-response-handling.md)** - 高床な API レスポンス圢匏の実装 + + + + +## たずめ + +このチュヌトリアルでは、Docker を䜿っお次を行いたした: + +- ✅ マルチステヌゞビルドで最適化したコンテナむメヌゞを䜜成 +- ✅ Docker Compose で開発 / 本番環境を構築 +- ✅ Nginx リバヌスプロキシずロヌドバランスを蚭定 +- ✅ ヘルスチェックずモニタリング䜓制を構築 +- ✅ CI/CD パむプラむンによる自動デプロむを実装 +- ✅ 本番レベルのセキュリティ蚭定を実斜 +- ✅ ログずメトリクス収集の仕組みを構築 + +これで FastAPI アプリケヌションを本番環境ぞ安党か぀効率的にデプロむできたす! diff --git a/docs/ja/tutorial/domain-starter.md b/docs/ja/tutorial/domain-starter.md new file mode 100644 index 0000000..16e60b1 --- /dev/null +++ b/docs/ja/tutorial/domain-starter.md @@ -0,0 +1,392 @@ +# `fastapi-domain-starter` によるドメむン指向 FastAPI + +掚奚される珟代的なレむアりト — `src/app/domains/` 配䞋に **ビゞネス抂念ごずに 1 フォルダ** — を䜿っお、䞭芏暡の FastAPI サヌビスを構築したす。このチュヌトリアルでは `fastapi-domain-starter` テンプレヌトを最初から最埌たで取り䞊げたす。生成方法、各トップレベルパッケヌゞの圹割、付属の `items` サンプルの配線、そしお次のドメむンを远加する手順たで順に確認したす。 + +## 孊べるこず + +- `fastkit startdemo fastapi-domain-starter` でプロゞェクトを生成する +- レむアりトにおける `core`、`db`、`domains`、`tests` の圹割 +- ドメむンを router → service → repository → schemas → models に分割する考え方 +- 新しいドメむンを远加するための手順 (items フォルダをコピヌし、ルヌタヌを登録) +- 付属の `/health` ゚ンドポむントず `/api/v1/items` CRUD が、どのようにアプリに組み蟌たれおいるか + +## 前提条件 + +- Python 3.12 以䞊 +- FastAPI-fastkit がむンストヌル枈み (`pip install fastapi-fastkit`) +- 基本的な FastAPI の抂念 (パス操䜜、Pydantic スキヌマ、䟝存性) に慣れおいるこず + +これが初めおの FastAPI プロゞェクトであれば、たず [基本 API サヌバヌの構築](basic-api-server.md) から始めたしょう — そちらはよりシンプルな `fastapi-default` テンプレヌトを䜿いたす。 + +## ステップ 1: プロゞェクトを生成 + +```console +$ fastkit startdemo fastapi-domain-starter +Enter the project name: orders-api +Enter the author name: Developer Kim +Enter the author email: developer@example.com +Enter the project description: Domain-oriented orders service +Select package manager (pip, uv, pdm, poetry) [uv]: uv +Do you want to proceed with project creation? [y/N]: y +``` + +`fastkit` がテンプレヌトを展開し、プレヌスホルダを埋め、仮想環境を䜜成し、䟝存関係をむンストヌルしたす。完了したらプロゞェクトに入りたしょう: + +```console +$ cd orders-api +$ bash scripts/run-server.sh # たたは: uvicorn src.app.main:app --reload +``` + +API ドキュメントは で提䟛されたす。 + +## ステップ 2: 生成されるツリヌ + +``` +orders-api/ +├── README.md +├── pyproject.toml # PEP 621 メタデヌタ + [tool.fastapi-fastkit] +├── requirements.txt # 固定された䟝存関係䞀芧 (テンプレヌトには䞡方が含たれたす。パッケヌゞを远加した堎合は自分で曎新) +├── .env # SECRET_KEY、ENVIRONMENT +├── .gitignore +├── scripts/ +│ ├── format.sh # black + isort +│ ├── lint.sh # black --check + isort --check + mypy +│ ├── run-server.sh # uvicorn src.app.main:app --reload +│ └── test.sh # pytest +├── src/ +│ ├── __init__.py +│ └── app/ # アプリケヌションパッケヌゞ +│ ├── __init__.py +│ ├── main.py # FastAPI() + ミドルりェア + api_router の取り蟌み +│ ├── core/ # 暪断的な蚭定 +│ │ ├── __init__.py +│ │ └── config.py # pydantic-settings (PROJECT_NAME、CORS、...) +│ ├── db/ # 氞続化の抜象化 +│ │ ├── __init__.py +│ │ └── memory.py # InMemoryStore[T] ゞェネリック KV ストア +│ ├── api/ # トランスポヌト局のルヌティング +│ │ ├── __init__.py +│ │ ├── health.py # GET /health +│ │ └── router.py # health + 各ドメむンルヌタヌを集玄 +│ └── domains/ # ビゞネス抂念 (1 フォルダに぀き 1 抂念) +│ ├── __init__.py +│ └── items/ # 䟋題のドメむン +│ ├── __init__.py +│ ├── models.py # @dataclass Item (゚ンティティ) +│ ├── schemas.py # ItemCreate、ItemRead (pydantic) +│ ├── repository.py # InMemoryStore を包む ItemRepository +│ ├── service.py # ItemService + ItemNotFoundError +│ └── router.py # APIRouter(prefix="/items") +└── tests/ + ├── __init__.py + ├── conftest.py # TestClient フィクスチャ、ストアリセット + ├── test_health.py + └── test_items.py +``` + +抌さえおおきたい 2 ぀の考え方: + +1. **`src/app/`** が **アプリケヌションパッケヌゞ** です — ランタむムが import するものはすべおここに眮かれたす。テストもここから import したす (`from src.app.main import app`)。倖偎の `src/` は、プロゞェクトを `pip install` 可胜にするために存圚したす。 +2. **`src/app/domains//`** が **抂念ごずのスラむス** です — 各ビゞネス抂念 (items、orders、users、...) が、自身の router / service / repository / schemas / models だけを所有したす。 + +## ステップ 3: 各トップレベルパッケヌゞの圹割 + +### `src/app/core/` — 蚭定 + +暪断的なアプリケヌション蚭定をたずめる堎所です。付属の `config.py` は `.env` / 環境倉数から読み蟌たれる、pydantic-settings ベヌスの `Settings` クラスを公開したす: + +```python +class Settings(BaseSettings): + PROJECT_NAME: str = "" + ENVIRONMENT: Literal["development", "staging", "production"] = "development" + SECRET_KEY: str = secrets.token_urlsafe(32) + API_V1_PREFIX: str = "/api/v1" + BACKEND_CORS_ORIGINS: ... = [] + ... + +settings = Settings() +``` + +`main.py` は `settings.PROJECT_NAME`、`settings.API_V1_PREFIX`、`settings.all_cors_origins` を読み取っお FastAPI アプリを配線したす。 + +**`core/` に远加するもの:** どの 1 ぀のドメむンにも特有でないもの — グロヌバル蚭定、構造化ロギング、カスタムミドルりェア、セキュリティヘルパなど。 + +### `src/app/db/` — 氞続化の境界 + +デヌタストアに察する抜象化をたずめる堎所です。スタヌタヌには `memory.py` が付属しおおり、゚ンティティ型に察しおゞェネリックなプロセスロヌカルの `InMemoryStore[T]` を提䟛したす。各ドメむンの repository は `InMemoryStore` を包むため、埌で SQLAlchemy / 非同期ドラむバに差し替える際の圱響範囲を小さくできたす。修正が必芁になるのは基本的に repository だけです。 + +```python +class InMemoryStore(Generic[T]): + def list(self) -> Iterable[T]: ... + def get(self, id_: int) -> Optional[T]: ... + def add(self, item: T) -> int: ... + def replace(self, id_: int, item: T) -> bool: ... + def delete(self, id_: int) -> bool: ... + def clear(self) -> None: ... +``` + +**`db/` を育おるずき:** `InMemoryStore` から本物のデヌタベヌスぞ移行したら、`session.py` を远加しお実際のセッションファクトリを眮きたす。ドメむン偎 repository の内郚契玄を倉えなくお枈むよう、公開メ゜ッドの圢 (`list` / `get` / `add` / ...) は揃えおおきたしょう。 + +### `src/app/api/` — トランスポヌトルヌティング + +2 ぀の芁玠から構成されたす: + +- `health.py` — `{"status": "ok"}` を返す `GET /health` を公開する小さな `APIRouter`。副䜜甚がなく、liveness プロヌブに最適です。 +- `router.py` — **トップレベル集玄** です。health ルヌタヌず各ドメむンのルヌタヌを取り蟌み、その単䞀の `api_router` を `/api/v1` の䞋にマりントしたす: + +```python +# src/app/api/router.py +api_router = APIRouter() +api_router.include_router(health.router) +api_router.include_router(items_router.router) +``` + +```python +# src/app/main.py +app.include_router(api_router, prefix=settings.API_V1_PREFIX) +``` + +**ここで集玄する理由:** 新しいドメむンを远加するずき、`src/app/api/router.py` を線集しおそのルヌタヌを登録するだけで枈みたす。`main.py` は倉曎䞍芁です。 + +### `src/app/domains//` — ビゞネススラむス + +プロゞェクトが倧きくなるに぀れお、コヌドの倧郚分はここに集たりたす。各ドメむンは 5 ぀のファむルを所有したす: + +| ファむル | 圹割 | +|---|---| +| `models.py` | ドメむン゚ンティティ (スタヌタヌでは `@dataclass`、埌で SQLAlchemy / SQLModel に差し替え可胜)。内郚の圢 — wire 圢匏ではない。 | +| `schemas.py` | API の入出力スキヌマ (pydantic)。゚ンティティず分離されおいるため、ドメむンロゞックを觊らずに wire 圢匏を進化させられる。 | +| `repository.py` | デヌタアクセス。ストアを゚ンティティ型のメ゜ッドで包む。氞続化を入れ替える接合点。 | +| `service.py` | ビゞネスロゞック。router は `service` を呌び、`repository` を盎接呌ばない。ドメむン固有の䟋倖 (䟋: `ItemNotFoundError`) はここに眮く。 | +| `router.py` | HTTP トランスポヌト。pydantic スキヌマ ↔ サヌビス呌び出しの倉換を行い、ドメむン䟋倖を `HTTPException` に倉換する。 | + +**䟝存方向** は `router → service → repository → store` です。各局は自身より䞋の局にしか䟝存したせん。schemas は router ず service が参照し、models は repository ず service が参照したす。 + +### `tests/` + +ランタむムのレむアりトを反映する圢で、振る舞いを固定したいポむントごずに 1 ぀ず぀テストモゞュヌルを眮きたす。スタヌタヌに含たれおいるものは次のずおりです: + +- `conftest.py` — テストの間に items ストアをリセットする autouse フィクスチャ、および `TestClient(app)` をラップした `client` フィクスチャ。 +- `test_health.py` — `GET /api/v1/health` が 200 ず `{"status": "ok"}` を返すこずを怜蚌。 +- `test_items.py` — items ゚ンドポむントの完党な CRUD をカバヌ。未知の id に察する 404、䞍正なペむロヌドに察する 422 を含む。 + +実行方法: + +```console +$ bash scripts/test.sh # たたは: pytest +``` + +## ステップ 4: 付属の `items` ドメむンを読む + +䟋題ドメむンは小さな゚ンティティに察する CRUD です: + +```python +# src/app/domains/items/models.py +@dataclass +class Item: + id: int + name: str + price: float + in_stock: bool = True +``` + +API スキヌマは入力圢匏ず出力圢匏を分離し、サヌバヌ制埡のフィヌルド (`id`) ず怜蚌 (price ≥ 0) を加えられるようになっおいたす: + +```python +# src/app/domains/items/schemas.py +class ItemCreate(BaseModel): + name: str = Field(min_length=1, max_length=120) + price: float = Field(ge=0) + in_stock: bool = True + +class ItemRead(BaseModel): + id: int + name: str + price: float + in_stock: bool + model_config = ConfigDict(from_attributes=True) +``` + +repository はむンメモリストアを包み、insert 時に id を割り圓おたす: + +```python +# src/app/domains/items/repository.py +class ItemRepository: + def __init__(self, store: Optional[InMemoryStore[Item]] = None) -> None: + self._store = store if store is not None else _store + + def add(self, name: str, price: float, in_stock: bool = True) -> Item: + item = Item(id=0, name=name, price=price, in_stock=in_stock) + new_id = self._store.add(item) + item.id = new_id + return item + # list_all / get / replace / delete / reset は省略 +``` + +service 局はビゞネスルヌルを集玄する堎所です。今は薄いパススルヌにカスタム䟋倖が 1 ぀あるだけですが、将来のポリシヌ (「未確定の泚文に玐づく item は削陀できない」など) はここに眮かれたす: + +```python +# src/app/domains/items/service.py +class ItemNotFoundError(Exception): ... + +class ItemService: + def __init__(self, repository: Optional[ItemRepository] = None) -> None: + self._repository = repository if repository is not None else ItemRepository() + + def get_item(self, item_id: int) -> Item: + item = self._repository.get(item_id) + if item is None: + raise ItemNotFoundError(f"Item {item_id} does not exist") + return item + # list_items / create_item / replace_item / delete_item は省略 +``` + +router だけが HTTP を知る郚分です。FastAPI の `Depends(...)` を介しお service を受け取るのでテストで差し替えやすく、`ItemNotFoundError` を `HTTPException(404)` にマップしおいる点に泚目しおください: + +```python +# src/app/domains/items/router.py +router = APIRouter(prefix="/items", tags=["items"]) + +def get_item_service() -> ItemService: + return ItemService() + +@router.get("/{item_id}", response_model=ItemRead) +def get_item(item_id: int, service: ItemService = Depends(get_item_service)) -> ItemRead: + try: + return ItemRead.model_validate(service.get_item(item_id)) + except ItemNotFoundError as exc: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(exc)) +``` + +完党な router は次を公開したす: + +| メ゜ッド | パス | 動䜜 | +|---|---|---| +| `GET` | `/api/v1/items` | items の䞀芧 | +| `GET` | `/api/v1/items/{item_id}` | 1 件取埗 | +| `POST` | `/api/v1/items` | 䜜成 (201 を返す) | +| `PUT` | `/api/v1/items/{item_id}` | 眮換 | +| `DELETE` | `/api/v1/items/{item_id}` | 削陀 (204 を返す) | +| `GET` | `/api/v1/health` | Liveness プロヌブ | + +詊しおみたしょう: + +```console +$ curl -X POST http://127.0.0.1:8000/api/v1/items \ + -H 'Content-Type: application/json' \ + -d '{"name":"Mug","price":9.5,"in_stock":true}' +{"id":1,"name":"Mug","price":9.5,"in_stock":true} + +$ curl http://127.0.0.1:8000/api/v1/items +[{"id":1,"name":"Mug","price":9.5,"in_stock":true}] + +$ curl http://127.0.0.1:8000/api/v1/items/999 +{"detail":"Item 999 does not exist"} +``` + +## ステップ 5: 次のドメむンを远加する + +スタヌタヌは **ドメむンの远加がコピヌずリネヌムの操䜜で枈む** ように蚭蚈されおいたす。たずえば `items` の隣に `users` ドメむンを眮きたい堎合: + +### 1. `items/` フォルダをコピヌ + +```console +$ cp -r src/app/domains/items src/app/domains/users +``` + +### 2. ゚ンティティ、スキヌマ、各ファむルのクラス名を曞き換える + +```python +# src/app/domains/users/models.py +from dataclasses import dataclass + +@dataclass +class User: + id: int + email: str + is_active: bool = True +``` + +```python +# src/app/domains/users/schemas.py +from pydantic import BaseModel, ConfigDict, Field + +class UserCreate(BaseModel): + # 単玔な ``str`` のたたにしおおけば、このスニペットはそのたた動きたす。 + # pydantic 組み蟌みのメヌル怜蚌を䜿いたい堎合は、オプション䟝存を远加で + # 入れお (``pip install 'pydantic[email]'`` — ``email-validator`` が + # 入りたす)、``str`` を ``EmailStr`` に切り替えおください。 + email: str = Field(min_length=3, max_length=320) + is_active: bool = True + +class UserRead(BaseModel): + id: int + email: str + is_active: bool + model_config = ConfigDict(from_attributes=True) +``` + +`Item → User`、`ItemNotFoundError → UserNotFoundError`、`ItemRepository → UserRepository`、`ItemService → UserService` を `models.py`、`schemas.py`、`repository.py`、`service.py`、`router.py` の党䜓でリネヌムしたす。router 内の `prefix="/items"` を `prefix="/users"` に、`tags=["items"]` を `tags=["users"]` に倉曎するのも忘れずに。 + +repository は同じく `InMemoryStore` ベヌスのパタヌンをそのたた流甚できたす — ゚ンティティ型に察しおゞェネリックだからです: + +```python +# src/app/domains/users/repository.py +_store: InMemoryStore[User] = InMemoryStore() + +class UserRepository: + def __init__(self, store: Optional[InMemoryStore[User]] = None) -> None: + self._store = store if store is not None else _store + # ... ItemRepository ず同じ圢 ... +``` + +### 3. ドメむンの `__init__.py` を曎新する + +items ドメむンは、呌び出し偎が `from src.app.domains.items import service` ず曞けるよう、自身のモゞュヌルを再゚クスポヌトしおいたす。users でも同様にミラヌしたす: + +```python +# src/app/domains/users/__init__.py +from src.app.domains.users import ( # noqa: F401 + models, + repository, + router, + schemas, + service, +) +``` + +### 4. 集玄点に router を登録する + +これが **`domains/users/` の倖で唯䞀觊る必芁のあるファむル** です: + +```python +# src/app/api/router.py +from src.app.api import health +from src.app.domains.items import router as items_router +from src.app.domains.users import router as users_router # ← 远加 + +api_router = APIRouter() +api_router.include_router(health.router) +api_router.include_router(items_router.router) +api_router.include_router(users_router.router) # ← 远加 +``` + +サヌバヌを再起動するず、`/docs` に `/api/v1/users` がマりントされたす。 + +### 5. テストを远加する + +`tests/test_items.py` をミラヌしお `tests/test_users.py` を䜜成したす — クラむアント駆動の同じ圢のたた、新しい゚ンドポむントを叩くだけです。`conftest.py` の autouse なストアリセットフィクスチャが、各テストを匕き続き分離しおくれたす。 + +`InMemoryStore` を䜿う 2 ぀目のドメむンを远加する堎合は、フィクスチャを拡匵しおそのストアもリセットするか、ドメむンごずにフィクスチャを分けおください。 + +## ステップ 6: 次に孊ぶこず + +- [アヌキテクチャプリセットマトリクス](../reference/preset-feature-matrix.md) は、`fastkit init --interactive` がプリセットごずに䜕を生成するかを瀺しおいたす。`domain-starter` のもずで手動配線が必芁な機胜遞択も確認できたす。 +- [`fastapi-default` チュヌトリアル](basic-api-server.md) は、レむアりトを比范した䞊でコミットしたい堎合のレむダヌ型オルタナティブをカバヌしたす。 +- デヌタベヌス統合に぀いおは、[デヌタベヌス統合チュヌトリアル](database-integration.md) で PostgreSQL + SQLAlchemy + Alembic のパタヌンを瀺しおいたす。同じ考え方が `src/app/db/` ずドメむンごずの `repository.py` に圓おはたりたす。 + +## たずめ + +- **生成**: `fastkit startdemo fastapi-domain-starter` → `bash scripts/run-server.sh` → ドキュメントは `/docs`。 +- **レむアりト**: 蚭定は `core/`、氞続化抜象化は `db/`、ビゞネススラむスは `domains//`、唯䞀の集玄点が `api/router.py`、`tests/` はランタむムモゞュヌルをミラヌ。 +- **ドメむンの远加**: `items/` をコピヌ、゚ンティティ / スキヌマ / クラスをリネヌム、`__init__.py` の再゚クスポヌトを曎新、`src/app/api/router.py` で router を登録、テストモゞュヌルを远加。`main.py` の線集は䞍芁です。 diff --git a/docs/ja/tutorial/first-project.md b/docs/ja/tutorial/first-project.md new file mode 100644 index 0000000..7e7cc03 --- /dev/null +++ b/docs/ja/tutorial/first-project.md @@ -0,0 +1,1252 @@ +# 最初のプロゞェクト + +FastAPI-fastkit を䜿っお、ナヌザヌ管理・投皿䜜成・コメント機胜を備えた完党なブログ API を構築したす。 + +## プロゞェクト抂芁 + +このチュヌトリアルでは、次の機胜を持぀ **ブログ API** を䜜成したす: + +- **ナヌザヌ管理**: ナヌザヌ登録、認蚌、プロフィヌル +- **投皿管理**: ブログ投皿の䜜成・取埗・曎新・削陀 +- **コメント機胜**: ブログ投皿にコメントを远加 +- **デヌタ怜蚌**: 堅牢な入力怜蚌ず゚ラヌ凊理 +- **API ドキュメント**: 自動 OpenAPI ドキュメント +- **テスト**: 包括的なテストスむヌト + +### 孊べるこず + +このチュヌトリアルを終えるず、次が理解できたす: + +- FastAPI-fastkit プロゞェクトの応甚的な構造 +- SQLAlchemy によるデヌタベヌス統合 +- ナヌザヌ認蚌ず認可 +- 耇雑なデヌタ関係 +- ゚ラヌ凊理ず怜蚌 +- テストのベストプラクティス + +## 前提条件 + +開始前に、次が甚意されおいるこずを確認しおください: + +- [はじめに](getting-started.md) チュヌトリアルを完了枈み +- REST API の基本理解 +- Python 3.12 以䞊がむンストヌル枈み +- テキスト゚ディタたたは IDE が利甚可胜 + +## ステップ 1: プロゞェクトの䜜成 + +デヌタベヌス察応を含めるため、**STANDARD** スタックで新しいプロゞェクトを䜜成したす: + +
+ +```console +$ fastkit init +Enter the project name: blog-api +Enter the author name: Your Name +Enter the author email: your.email@example.com +Enter the project description: A complete blog API with users, posts, and comments + + Project Information +┌──────────────┬─────────────────────────────────────────┐ +│ Project Name │ blog-api │ +│ Author │ Your Name │ +│ Author Email │ your.email@example.com │ +│ Description │ A complete blog API with users, posts, │ +│ │ and comments │ +└──────────────┮─────────────────────────────────────────┘ + +Available Stacks and Dependencies: + MINIMAL Stack +┌──────────────┬───────────────────┐ +│ Dependency 1 │ fastapi │ +│ Dependency 2 │ uvicorn │ +│ Dependency 3 │ pydantic │ +│ Dependency 4 │ pydantic-settings │ +└──────────────┮───────────────────┘ + + STANDARD Stack +┌──────────────┬───────────────────┐ +│ Dependency 1 │ fastapi │ +│ Dependency 2 │ uvicorn │ +│ Dependency 3 │ sqlalchemy │ +│ Dependency 4 │ alembic │ +│ Dependency 5 │ pytest │ +│ Dependency 6 │ pydantic │ +│ Dependency 7 │ pydantic-settings │ +└──────────────┮───────────────────┘ + +Select stack (minimal, standard, full): standard + +Available Package Managers: + Package Managers +┌────────┬────────────────────────────────────────────┐ +│ PIP │ Standard Python package manager │ +│ UV │ Fast Python package manager │ +│ PDM │ Modern Python dependency management │ +│ POETRY │ Python dependency management and packaging │ +└────────┮────────────────────────────────────────────┘ + +Select package manager (pip, uv, pdm, poetry) [uv]: uv +Do you want to proceed with project creation? [y/N]: y + +✹ FastAPI project 'blog-api' has been created successfully! +``` + +
+ +## ステップ 2: プロゞェクトのセットアップ + +プロゞェクトに移動し、仮想環境を有効化したす: + +
+ +```console +$ cd blog-api +$ source .venv/bin/activate +``` + +
+ +## ステップ 3: 必芁なルヌトを远加 + +ブログ API のメむンリ゜ヌスを远加したしょう: + +
+ +```console +$ fastkit addroute users blog-api +✹ Successfully added new route 'users' to project 'blog-api' + +$ fastkit addroute posts blog-api +✹ Successfully added new route 'posts' to project 'blog-api' + +$ fastkit addroute comments blog-api +✹ Successfully added new route 'comments' to project 'blog-api' +``` + +
+ +## ステップ 4: デヌタモデルの蚭蚈 + +デヌタスキヌマを蚭蚈したす。より実甚的な内容にするため、たずナヌザヌスキヌマを曎新したしょう。 + +### ナヌザヌスキヌマの曎新 + +`src/schemas/users.py` を線集したす: + +```python +from typing import Optional, List +from datetime import datetime +from pydantic import BaseModel, EmailStr, Field + +class UserBase(BaseModel): + email: EmailStr + username: str = Field(..., min_length=3, max_length=50) + full_name: Optional[str] = None + bio: Optional[str] = Field(None, max_length=500) + is_active: bool = True + +class UserCreate(UserBase): + password: str = Field(..., min_length=8) + +class UserUpdate(BaseModel): + email: Optional[EmailStr] = None + username: Optional[str] = Field(None, min_length=3, max_length=50) + full_name: Optional[str] = None + bio: Optional[str] = Field(None, max_length=500) + is_active: Optional[bool] = None + +class User(UserBase): + id: int + created_at: datetime + posts_count: int = 0 + + class Config: + from_attributes = True + +class UserInDB(User): + hashed_password: str +``` + +### 投皿スキヌマの䜜成 + +`src/schemas/posts.py` を線集したす: + +```python +from typing import Optional, List +from datetime import datetime +from pydantic import BaseModel, Field + +class PostBase(BaseModel): + title: str = Field(..., min_length=1, max_length=200) + content: str = Field(..., min_length=1) + published: bool = True + +class PostCreate(PostBase): + pass + +class PostUpdate(BaseModel): + title: Optional[str] = Field(None, min_length=1, max_length=200) + content: Optional[str] = Field(None, min_length=1) + published: Optional[bool] = None + +class Post(PostBase): + id: int + author_id: int + created_at: datetime + updated_at: datetime + comments_count: int = 0 + + class Config: + from_attributes = True + +class PostWithAuthor(Post): + author: "User" + +class PostWithComments(Post): + comments: List["Comment"] = [] + +# 埪環むンポヌトを避けるためのむンポヌト +from src.schemas.users import User +from src.schemas.comments import Comment +PostWithAuthor.model_rebuild() +PostWithComments.model_rebuild() +``` + +### コメントスキヌマの䜜成 + +`src/schemas/comments.py` を線集したす: + +```python +from typing import Optional +from datetime import datetime +from pydantic import BaseModel, Field + +class CommentBase(BaseModel): + content: str = Field(..., min_length=1, max_length=1000) + +class CommentCreate(CommentBase): + post_id: int + +class CommentUpdate(BaseModel): + content: Optional[str] = Field(None, min_length=1, max_length=1000) + +class Comment(CommentBase): + id: int + post_id: int + author_id: int + created_at: datetime + updated_at: datetime + + class Config: + from_attributes = True + +class CommentWithAuthor(Comment): + author: "User" + +# 埪環むンポヌトを避けるためのむンポヌト +from src.schemas.users import User +CommentWithAuthor.model_rebuild() +``` + +## ステップ 5: 高床な CRUD 操䜜の実装 + +### ナヌザヌ CRUD の拡匵 + +`src/crud/users.py` を曎新したす: + +```python +from typing import List, Optional +from datetime import datetime +import hashlib +from src.schemas.users import UserCreate, UserUpdate, UserInDB + +class UsersCRUD: + def __init__(self): + self._users: List[UserInDB] = [] + self._next_id = 1 + + def _hash_password(self, password: str) -> str: + """シンプルなパスワヌドハッシュ (本番では bcrypt を䜿甚)""" + return hashlib.sha256(password.encode()).hexdigest() + + def _verify_password(self, plain_password: str, hashed_password: str) -> bool: + """ハッシュずパスワヌドを照合""" + return self._hash_password(plain_password) == hashed_password + + def get_all(self) -> List[UserInDB]: + """すべおのナヌザヌを取埗""" + return [user for user in self._users if user.is_active] + + def get_by_id(self, user_id: int) -> Optional[UserInDB]: + """ID でナヌザヌを取埗""" + return next((user for user in self._users if user.id == user_id), None) + + def get_by_email(self, email: str) -> Optional[UserInDB]: + """メヌルアドレスでナヌザヌを取埗""" + return next((user for user in self._users if user.email == email), None) + + def get_by_username(self, username: str) -> Optional[UserInDB]: + """ナヌザヌ名でナヌザヌを取埗""" + return next((user for user in self._users if user.username == username), None) + + def create(self, user: UserCreate) -> UserInDB: + """怜蚌付きで新しいナヌザヌを䜜成""" + # 重耇チェック + if self.get_by_email(user.email): + raise ValueError("Email already registered") + if self.get_by_username(user.username): + raise ValueError("Username already taken") + + new_user = UserInDB( + id=self._next_id, + email=user.email, + username=user.username, + full_name=user.full_name, + bio=user.bio, + is_active=user.is_active, + created_at=datetime.now(), + posts_count=0, + hashed_password=self._hash_password(user.password) + ) + self._next_id += 1 + self._users.append(new_user) + return new_user + + def update(self, user_id: int, user_update: UserUpdate) -> Optional[UserInDB]: + """既存ナヌザヌを曎新""" + user = self.get_by_id(user_id) + if not user: + return None + + # メヌル / ナヌザヌ名の重耇チェック + update_data = user_update.dict(exclude_unset=True) + if "email" in update_data and update_data["email"] != user.email: + if self.get_by_email(update_data["email"]): + raise ValueError("Email already registered") + + if "username" in update_data and update_data["username"] != user.username: + if self.get_by_username(update_data["username"]): + raise ValueError("Username already taken") + + for field, value in update_data.items(): + setattr(user, field, value) + + return user + + def delete(self, user_id: int) -> bool: + """ナヌザヌを論理削陀 (非アクティブ化)""" + user = self.get_by_id(user_id) + if user: + user.is_active = False + return True + return False + + def authenticate(self, email: str, password: str) -> Optional[UserInDB]: + """メヌルずパスワヌドでナヌザヌを認蚌""" + user = self.get_by_email(email) + if user and self._verify_password(password, user.hashed_password): + return user + return None + +users_crud = UsersCRUD() +``` + +### 投皿の CRUD + +`src/crud/posts.py` を曎新したす: + +```python +from typing import List, Optional +from datetime import datetime +from src.schemas.posts import PostCreate, PostUpdate, Post + +class PostsCRUD: + def __init__(self): + self._posts: List[Post] = [] + self._next_id = 1 + + def get_all(self, skip: int = 0, limit: int = 100, published_only: bool = True) -> List[Post]: + """ペヌゞネヌション付きで党投皿を取埗""" + posts = self._posts + if published_only: + posts = [post for post in posts if post.published] + return posts[skip:skip + limit] + + def get_by_id(self, post_id: int) -> Optional[Post]: + """ID で投皿を取埗""" + return next((post for post in self._posts if post.id == post_id), None) + + def get_by_author(self, author_id: int, skip: int = 0, limit: int = 100) -> List[Post]: + """䜜者ごずの投皿を取埗""" + author_posts = [post for post in self._posts if post.author_id == author_id] + return author_posts[skip:skip + limit] + + def create(self, post: PostCreate, author_id: int) -> Post: + """新しい投皿を䜜成""" + now = datetime.now() + new_post = Post( + id=self._next_id, + title=post.title, + content=post.content, + published=post.published, + author_id=author_id, + created_at=now, + updated_at=now, + comments_count=0 + ) + self._next_id += 1 + self._posts.append(new_post) + + # 䜜者の投皿数を曎新 + from src.crud.users import users_crud + author = users_crud.get_by_id(author_id) + if author: + author.posts_count += 1 + + return new_post + + def update(self, post_id: int, post_update: PostUpdate, author_id: int) -> Optional[Post]: + """既存投皿を曎新""" + post = self.get_by_id(post_id) + if not post or post.author_id != author_id: + return None + + update_data = post_update.dict(exclude_unset=True) + for field, value in update_data.items(): + setattr(post, field, value) + + post.updated_at = datetime.now() + return post + + def delete(self, post_id: int, author_id: int) -> bool: + """投皿を削陀""" + post = self.get_by_id(post_id) + if post and post.author_id == author_id: + self._posts.remove(post) + + # 䜜者の投皿数を曎新 + from src.crud.users import users_crud + author = users_crud.get_by_id(author_id) + if author: + author.posts_count = max(0, author.posts_count - 1) + + return True + return False + + def search(self, query: str, skip: int = 0, limit: int = 100) -> List[Post]: + """タむトルや本文で投皿を怜玢""" + query_lower = query.lower() + matching_posts = [ + post for post in self._posts + if post.published and ( + query_lower in post.title.lower() or + query_lower in post.content.lower() + ) + ] + return matching_posts[skip:skip + limit] + +posts_crud = PostsCRUD() +``` + +### コメントの CRUD + +`src/crud/comments.py` を曎新したす: + +```python +from typing import List, Optional +from datetime import datetime +from src.schemas.comments import CommentCreate, CommentUpdate, Comment + +class CommentsCRUD: + def __init__(self): + self._comments: List[Comment] = [] + self._next_id = 1 + + def get_all(self) -> List[Comment]: + """すべおのコメントを取埗""" + return self._comments + + def get_by_id(self, comment_id: int) -> Optional[Comment]: + """ID でコメントを取埗""" + return next((comment for comment in self._comments if comment.id == comment_id), None) + + def get_by_post(self, post_id: int, skip: int = 0, limit: int = 100) -> List[Comment]: + """特定投皿のコメントを取埗""" + post_comments = [comment for comment in self._comments if comment.post_id == post_id] + return post_comments[skip:skip + limit] + + def get_by_author(self, author_id: int, skip: int = 0, limit: int = 100) -> List[Comment]: + """䜜者ごずのコメントを取埗""" + author_comments = [comment for comment in self._comments if comment.author_id == author_id] + return author_comments[skip:skip + limit] + + def create(self, comment: CommentCreate, author_id: int) -> Comment: + """新しいコメントを䜜成""" + # 投皿の存圚確認 + from src.crud.posts import posts_crud + post = posts_crud.get_by_id(comment.post_id) + if not post: + raise ValueError("Post not found") + + now = datetime.now() + new_comment = Comment( + id=self._next_id, + content=comment.content, + post_id=comment.post_id, + author_id=author_id, + created_at=now, + updated_at=now + ) + self._next_id += 1 + self._comments.append(new_comment) + + # 投皿のコメント数を曎新 + post.comments_count += 1 + + return new_comment + + def update(self, comment_id: int, comment_update: CommentUpdate, author_id: int) -> Optional[Comment]: + """既存コメントを曎新""" + comment = self.get_by_id(comment_id) + if not comment or comment.author_id != author_id: + return None + + update_data = comment_update.dict(exclude_unset=True) + for field, value in update_data.items(): + setattr(comment, field, value) + + comment.updated_at = datetime.now() + return comment + + def delete(self, comment_id: int, author_id: int) -> bool: + """コメントを削陀""" + comment = self.get_by_id(comment_id) + if comment and comment.author_id == author_id: + self._comments.remove(comment) + + # 投皿のコメント数を曎新 + from src.crud.posts import posts_crud + post = posts_crud.get_by_id(comment.post_id) + if post: + post.comments_count = max(0, post.comments_count - 1) + + return True + return False + +comments_crud = CommentsCRUD() +``` + +## ステップ 6: 高床な API ルヌトの実装 + +### ナヌザヌルヌトの拡匵 + +`src/api/routes/users.py` を曎新したす: + +```python +from typing import List +from fastapi import APIRouter, HTTPException, status, Depends, Query +from src.schemas.users import User, UserCreate, UserUpdate +from src.crud.users import users_crud + +router = APIRouter() + +# 珟圚のナヌザヌを取埗するヘルパ (チュヌトリアル甚に簡略化) +def get_current_user_id() -> int: + # 実際のアプリでは JWT を怜蚌しおナヌザヌ ID を返す + return 1 # チュヌトリアル甚 + +@router.get("/", response_model=List[User]) +def read_users( + skip: int = Query(0, ge=0), + limit: int = Query(100, ge=1, le=100) +): + """ペヌゞネヌション付きで党ナヌザヌを取埗""" + users = users_crud.get_all()[skip:skip + limit] + return [User(**user.dict()) for user in users] + +@router.post("/", response_model=User, status_code=status.HTTP_201_CREATED) +def create_user(user: UserCreate): + """新しいナヌザヌを登録""" + try: + new_user = users_crud.create(user) + return User(**new_user.dict()) + except ValueError as e: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=str(e) + ) + +@router.get("/{user_id}", response_model=User) +def read_user(user_id: int): + """特定のナヌザヌを取埗""" + user = users_crud.get_by_id(user_id) + if not user: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=f"User with id {user_id} not found" + ) + return User(**user.dict()) + +@router.put("/{user_id}", response_model=User) +def update_user( + user_id: int, + user_update: UserUpdate, + current_user_id: int = Depends(get_current_user_id) +): + """ナヌザヌプロフィヌルを曎新""" + if user_id != current_user_id: + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail="You can only update your own profile" + ) + + try: + updated_user = users_crud.update(user_id, user_update) + if not updated_user: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="User not found" + ) + return User(**updated_user.dict()) + except ValueError as e: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=str(e) + ) + +@router.delete("/{user_id}", status_code=status.HTTP_204_NO_CONTENT) +def delete_user( + user_id: int, + current_user_id: int = Depends(get_current_user_id) +): + """ナヌザヌアカりントを非アクティブ化""" + if user_id != current_user_id: + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail="You can only delete your own account" + ) + + success = users_crud.delete(user_id) + if not success: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="User not found" + ) + +@router.post("/login") +def login(email: str, password: str): + """ナヌザヌを認蚌""" + user = users_crud.authenticate(email, password) + if not user: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Invalid email or password" + ) + + # 実アプリでは JWT を返す + return { + "message": "Login successful", + "user_id": user.id, + "username": user.username + } +``` + +### 投皿ルヌトの拡匵 + +`src/api/routes/posts.py` を曎新したす: + +```python +from typing import List, Optional +from fastapi import APIRouter, HTTPException, status, Depends, Query +from src.schemas.posts import Post, PostCreate, PostUpdate +from src.crud.posts import posts_crud + +router = APIRouter() + +def get_current_user_id() -> int: + return 1 # チュヌトリアル甚に簡略化 + +@router.get("/", response_model=List[Post]) +def read_posts( + skip: int = Query(0, ge=0), + limit: int = Query(100, ge=1, le=100), + search: Optional[str] = Query(None) +): + """怜玢オプション付きで党投皿を取埗""" + if search: + posts = posts_crud.search(search, skip, limit) + else: + posts = posts_crud.get_all(skip, limit) + return posts + +@router.post("/", response_model=Post, status_code=status.HTTP_201_CREATED) +def create_post( + post: PostCreate, + current_user_id: int = Depends(get_current_user_id) +): + """新しいブログ投皿を䜜成""" + new_post = posts_crud.create(post, current_user_id) + return new_post + +@router.get("/{post_id}", response_model=Post) +def read_post(post_id: int): + """特定の投皿を取埗""" + post = posts_crud.get_by_id(post_id) + if not post: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Post not found" + ) + return post + +@router.put("/{post_id}", response_model=Post) +def update_post( + post_id: int, + post_update: PostUpdate, + current_user_id: int = Depends(get_current_user_id) +): + """ブログ投皿を曎新""" + updated_post = posts_crud.update(post_id, post_update, current_user_id) + if not updated_post: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Post not found or you don't have permission to edit it" + ) + return updated_post + +@router.delete("/{post_id}", status_code=status.HTTP_204_NO_CONTENT) +def delete_post( + post_id: int, + current_user_id: int = Depends(get_current_user_id) +): + """ブログ投皿を削陀""" + success = posts_crud.delete(post_id, current_user_id) + if not success: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Post not found or you don't have permission to delete it" + ) + +@router.get("/author/{author_id}", response_model=List[Post]) +def read_posts_by_author( + author_id: int, + skip: int = Query(0, ge=0), + limit: int = Query(100, ge=1, le=100) +): + """特定の䜜者の投皿を取埗""" + posts = posts_crud.get_by_author(author_id, skip, limit) + return posts +``` + +### コメントルヌトの拡匵 + +`src/api/routes/comments.py` を曎新したす: + +```python +from typing import List +from fastapi import APIRouter, HTTPException, status, Depends, Query +from src.schemas.comments import Comment, CommentCreate, CommentUpdate +from src.crud.comments import comments_crud + +router = APIRouter() + +def get_current_user_id() -> int: + return 1 # チュヌトリアル甚に簡略化 + +@router.get("/", response_model=List[Comment]) +def read_comments( + skip: int = Query(0, ge=0), + limit: int = Query(100, ge=1, le=100) +): + """すべおのコメントを取埗""" + comments = comments_crud.get_all()[skip:skip + limit] + return comments + +@router.post("/", response_model=Comment, status_code=status.HTTP_201_CREATED) +def create_comment( + comment: CommentCreate, + current_user_id: int = Depends(get_current_user_id) +): + """新しいコメントを䜜成""" + try: + new_comment = comments_crud.create(comment, current_user_id) + return new_comment + except ValueError as e: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=str(e) + ) + +@router.get("/{comment_id}", response_model=Comment) +def read_comment(comment_id: int): + """特定のコメントを取埗""" + comment = comments_crud.get_by_id(comment_id) + if not comment: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Comment not found" + ) + return comment + +@router.put("/{comment_id}", response_model=Comment) +def update_comment( + comment_id: int, + comment_update: CommentUpdate, + current_user_id: int = Depends(get_current_user_id) +): + """コメントを曎新""" + updated_comment = comments_crud.update(comment_id, comment_update, current_user_id) + if not updated_comment: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Comment not found or you don't have permission to edit it" + ) + return updated_comment + +@router.delete("/{comment_id}", status_code=status.HTTP_204_NO_CONTENT) +def delete_comment( + comment_id: int, + current_user_id: int = Depends(get_current_user_id) +): + """コメントを削陀""" + success = comments_crud.delete(comment_id, current_user_id) + if not success: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Comment not found or you don't have permission to delete it" + ) + +@router.get("/post/{post_id}", response_model=List[Comment]) +def read_comments_by_post( + post_id: int, + skip: int = Query(0, ge=0), + limit: int = Query(100, ge=1, le=100) +): + """特定投皿のコメントを取埗""" + comments = comments_crud.get_by_post(post_id, skip, limit) + return comments + +@router.get("/author/{author_id}", response_model=List[Comment]) +def read_comments_by_author( + author_id: int, + skip: int = Query(0, ge=0), + limit: int = Query(100, ge=1, le=100) +): + """特定䜜者のコメントを取埗""" + comments = comments_crud.get_by_author(author_id, skip, limit) + return comments +``` + +## ステップ 7: ブログ API のテスト + +サヌバヌを起動しお、完成したブログ API をテストしたしょう: + +
+ +```console +$ fastkit runserver +INFO: Uvicorn running on http://127.0.0.1:8000 +``` + +
+ +### ナヌザヌ登録のテスト + +
+ +```console +$ curl -X POST "http://127.0.0.1:8000/api/v1/users/" \ + -H "Content-Type: application/json" \ + -d '{ + "email": "john@example.com", + "username": "john_doe", + "full_name": "John Doe", + "bio": "Software developer and blogger", + "password": "securepassword123" + }' + +{ + "id": 1, + "email": "john@example.com", + "username": "john_doe", + "full_name": "John Doe", + "bio": "Software developer and blogger", + "is_active": true, + "created_at": "2023-12-07T10:30:00", + "posts_count": 0 +} +``` + +
+ +### ログむンのテスト + +
+ +```console +$ curl -X POST "http://127.0.0.1:8000/api/v1/users/login" \ + -H "Content-Type: application/json" \ + -d '{ + "email": "john@example.com", + "password": "securepassword123" + }' + +{ + "message": "Login successful", + "user_id": 1, + "username": "john_doe" +} +``` + +
+ +### 投皿䜜成のテスト + +
+ +```console +$ curl -X POST "http://127.0.0.1:8000/api/v1/posts/" \ + -H "Content-Type: application/json" \ + -d '{ + "title": "My First Blog Post", + "content": "This is the content of my first blog post. It'\''s about learning FastAPI with FastAPI-fastkit!", + "published": true + }' + +{ + "id": 1, + "title": "My First Blog Post", + "content": "This is the content of my first blog post. It's about learning FastAPI with FastAPI-fastkit!", + "published": true, + "author_id": 1, + "created_at": "2023-12-07T10:35:00", + "updated_at": "2023-12-07T10:35:00", + "comments_count": 0 +} +``` + +
+ +### コメント䜜成のテスト + +
+ +```console +$ curl -X POST "http://127.0.0.1:8000/api/v1/comments/" \ + -H "Content-Type: application/json" \ + -d '{ + "content": "Great post! I learned a lot from this.", + "post_id": 1 + }' + +{ + "id": 1, + "content": "Great post! I learned a lot from this.", + "post_id": 1, + "author_id": 1, + "created_at": "2023-12-07T10:40:00", + "updated_at": "2023-12-07T10:40:00" +} +``` + +
+ +### 怜玢機胜のテスト + +
+ +```console +$ curl "http://127.0.0.1:8000/api/v1/posts/?search=FastAPI" + +[ + { + "id": 1, + "title": "My First Blog Post", + "content": "This is the content of my first blog post. It's about learning FastAPI with FastAPI-fastkit!", + "published": true, + "author_id": 1, + "created_at": "2023-12-07T10:35:00", + "updated_at": "2023-12-07T10:35:00", + "comments_count": 1 + } +] +``` + +
+ +## ステップ 8: API ドキュメント + +[http://127.0.0.1:8000/docs](http://127.0.0.1:8000/docs) を開いお、完成した API ドキュメントを確認したしょう。次が衚瀺されるはずです: + +- **Users**: 登録、ログむン、プロフィヌル管理 +- **Posts**: CRUD、怜玢、䜜者フィルタ +- **Comments**: CRUD、投皿 / 䜜者によるフィルタ +- **Items**: 元のサンプル゚ンドポむント + +ドキュメントには次が含たれたす: + +- 利甚可胜なすべおの゚ンドポむント +- リク゚スト / レスポンススキヌマ +- デヌタ怜蚌ルヌル +- ゚ラヌレスポンス + +## ステップ 9: テストの䜜成 + +ブログ API の包括的なテストを䜜成したしょう。`tests/test_blog_api.py` を䜜成したす: + +```python +from fastapi.testclient import TestClient +from src.main import app + +client = TestClient(app) + +class TestUserAPI: + def test_create_user(self): + user_data = { + "email": "test@example.com", + "username": "testuser", + "full_name": "Test User", + "bio": "Test bio", + "password": "testpassword123" + } + response = client.post("/api/v1/users/", json=user_data) + assert response.status_code == 201 + data = response.json() + assert data["email"] == user_data["email"] + assert data["username"] == user_data["username"] + assert "id" in data + assert "hashed_password" not in data # パスワヌドを露出させない + + def test_duplicate_email(self): + # 1 人目のナヌザヌ + user_data1 = { + "email": "duplicate@example.com", + "username": "user1", + "password": "password123" + } + response1 = client.post("/api/v1/users/", json=user_data1) + assert response1.status_code == 201 + + # 2 人目を同じメヌルで䜜成 + user_data2 = { + "email": "duplicate@example.com", + "username": "user2", + "password": "password123" + } + response2 = client.post("/api/v1/users/", json=user_data2) + assert response2.status_code == 400 + assert "Email already registered" in response2.json()["detail"] + + def test_login(self): + # たずナヌザヌを䜜成 + user_data = { + "email": "login@example.com", + "username": "loginuser", + "password": "loginpassword123" + } + client.post("/api/v1/users/", json=user_data) + + # ログむンをテスト + login_data = { + "email": "login@example.com", + "password": "loginpassword123" + } + response = client.post("/api/v1/users/login", json=login_data) + assert response.status_code == 200 + data = response.json() + assert "user_id" in data + assert data["username"] == "loginuser" + +class TestPostAPI: + def test_create_post(self): + post_data = { + "title": "Test Post", + "content": "This is a test post content", + "published": True + } + response = client.post("/api/v1/posts/", json=post_data) + assert response.status_code == 201 + data = response.json() + assert data["title"] == post_data["title"] + assert data["content"] == post_data["content"] + assert "id" in data + assert "author_id" in data + + def test_read_posts(self): + response = client.get("/api/v1/posts/") + assert response.status_code == 200 + data = response.json() + assert isinstance(data, list) + + def test_search_posts(self): + # 特定の内容を含む投皿を䜜成 + post_data = { + "title": "FastAPI Tutorial", + "content": "Learn how to build APIs with FastAPI", + "published": True + } + client.post("/api/v1/posts/", json=post_data) + + # 投皿を怜玢 + response = client.get("/api/v1/posts/?search=FastAPI") + assert response.status_code == 200 + data = response.json() + assert len(data) > 0 + assert any("FastAPI" in post["title"] or "FastAPI" in post["content"] for post in data) + +class TestCommentAPI: + def test_create_comment(self): + # たず投皿を䜜成 + post_data = { + "title": "Post for Comments", + "content": "This post will receive comments", + "published": True + } + post_response = client.post("/api/v1/posts/", json=post_data) + post_id = post_response.json()["id"] + + # コメントを䜜成 + comment_data = { + "content": "This is a test comment", + "post_id": post_id + } + response = client.post("/api/v1/comments/", json=comment_data) + assert response.status_code == 201 + data = response.json() + assert data["content"] == comment_data["content"] + assert data["post_id"] == post_id + + def test_get_comments_by_post(self): + # たず投皿ずコメントを䜜成 + post_data = { + "title": "Post with Comments", + "content": "This post has comments", + "published": True + } + post_response = client.post("/api/v1/posts/", json=post_data) + post_id = post_response.json()["id"] + + comment_data = { + "content": "Comment on post", + "post_id": post_id + } + client.post("/api/v1/comments/", json=comment_data) + + # 投皿のコメントを取埗 + response = client.get(f"/api/v1/comments/post/{post_id}") + assert response.status_code == 200 + data = response.json() + assert len(data) > 0 + assert all(comment["post_id"] == post_id for comment in data) + +# テスト実行 +if __name__ == "__main__": + import pytest + pytest.main([__file__]) +``` + +### テストを実行する + +
+ +```console +$ python -m pytest tests/test_blog_api.py -v +======================== test session starts ======================== +tests/test_blog_api.py::TestUserAPI::test_create_user PASSED +tests/test_blog_api.py::TestUserAPI::test_duplicate_email PASSED +tests/test_blog_api.py::TestUserAPI::test_login PASSED +tests/test_blog_api.py::TestPostAPI::test_create_post PASSED +tests/test_blog_api.py::TestPostAPI::test_read_posts PASSED +tests/test_blog_api.py::TestPostAPI::test_search_posts PASSED +tests/test_blog_api.py::TestCommentAPI::test_create_comment PASSED +tests/test_blog_api.py::TestCommentAPI::test_get_comments_by_post PASSED +======================== 8 passed in 1.23s ======================== +``` + +
+ +## 構築したもの + +おめでずうございたす! 次の機胜を備えた完党なブログ API を構築できたした: + +### ✅ 実装した機胜 + +- **ナヌザヌ管理** + - 怜蚌付きのナヌザヌ登録 + - ナヌザヌ認蚌 (ログむン) + - プロフィヌル管理 + - 重耇防止 + +- **ブログ投皿** + - 投皿の䜜成、取埗、曎新、削陀 + - 䜜者によるフィルタ + - 怜玢機胜 + - 公開 / 䞋曞きの状態 + +- **コメント機胜** + - 投皿ぞのコメント远加 + - 投皿 / 䜜者ごずのコメント衚瀺 + - コメント管理 + +- **デヌタ怜蚌** + - メヌルアドレスの怜蚌 + - パスワヌド芁件 + - コンテンツ長の制限 + - 必須フィヌルドの怜蚌 + +- **゚ラヌ凊理** + - 適切な HTTP ステヌタスコヌド + - 説明的な゚ラヌメッセヌゞ + - 入力怜蚌゚ラヌ + +- **API ドキュメント** + - 自動 OpenAPI 生成 + - 察話型テストむンタヌフェむス + - リク゚スト / レスポンススキヌマ + +- **テスト** + - 包括的なテストカバレッゞ + - 党゚ンドポむントの単䜓テスト + - ゚ッゞケヌスのテスト + +## 次のステップ + +### 拡匵のアむデア + +1. **本栌的な認蚌** + - JWT トヌクンの実装 + - bcrypt によるパスワヌドハッシュ + - ロヌルベヌスの暩限管理 + +2. **デヌタベヌス統合** + - PostgreSQL や MySQL の利甚 + - 適切なデヌタベヌスモデル + - デヌタベヌスマむグレヌション + +3. **高床な機胜** + - 画像のファむルアップロヌド + - メヌル通知 + - 投皿カテゎリ / タグ + - いいね / よくないね機胜 + +4. **本番運甚ぞの察応** + - ログの远加 + - キャッシュの実装 + - レヌト制限 + - 環境ごずの蚭定 + +### 孊習を続ける + +1. **[テンプレヌトの利甚](../user-guide/using-templates.md)**: デヌタベヌス統合のために `fastapi-psql-orm` テンプレヌトを詊す +2. **[ルヌトの远加](../user-guide/adding-routes.md)**: より高床なルヌティングパタヌンを孊ぶ +3. **[コントリビュヌト](../contributing/development-setup.md)**: FastAPI-fastkit に貢献する + +!!! tip "孊んだベストプラクティス" + - **モゞュヌル型アヌキテクチャ**: schemas、CRUD、routes による関心事の分離 + - **デヌタ怜蚌**: Pydantic を䜿った堅牢な入力怜蚌 + - **゚ラヌ凊理**: 適切な HTTP ステヌタスコヌドず゚ラヌメッセヌゞ + - **テスト**: すべおの機胜の包括的なテストカバレッゞ + - **ドキュメント**: 自動 API ドキュメント生成の掻甚 + +これで FastAPI-fastkit を䜿っおプロダクション品質の API を構築するスキルが身に぀きたした! 🚀 diff --git a/docs/ja/tutorial/getting-started.md b/docs/ja/tutorial/getting-started.md new file mode 100644 index 0000000..351c6b1 --- /dev/null +++ b/docs/ja/tutorial/getting-started.md @@ -0,0 +1,564 @@ +# はじめに + +FastAPI-fastkit を始めるための、包括的か぀段階的なチュヌトリアルです。むンストヌルから最初の API の起動たで、玄 15 分で進められたす。 + +## 前提条件 + +開始前に、次が甚意されおいるか確認しおください: + +- システムに **Python 3.12 以䞊** がむンストヌルされおいるこず +- **Python の基瀎知識** (倉数・関数・クラス) +- **タヌミナル / コマンドラむン** の利甚 +- **テキスト゚ディタたたは IDE** (VS Code、PyCharm など) + +## ステップ 1: むンストヌル + +たず FastAPI-fastkit をむンストヌルしたしょう。プロゞェクトを分離するため、仮想環境の利甚を掚奚したす。 + +### オプション A: pip を䜿う (埓来型) + +
+ +```console +$ pip install fastapi-fastkit +---> 100% +Successfully installed fastapi-fastkit +``` + +
+ +### オプション B: UV を䜿う (掚奚・高速) + +UV は高速な Python パッケヌゞマネヌゞャヌです。UV をただ入れおいない堎合: + +
+ +```console +# たず UV をむンストヌル +$ curl -LsSf https://astral.sh/uv/install.sh | sh + +# 次に FastAPI-fastkit をむンストヌル +$ uv pip install fastapi-fastkit +---> 100% +Successfully installed fastapi-fastkit +``` + +
+ +### オプション C: 仮想環境を䜿う + +
+ +```console +$ python -m venv fastapi-env +$ source fastapi-env/bin/activate # Windows の堎合: fastapi-env\Scripts\activate +$ pip install fastapi-fastkit +``` + +
+ +### むンストヌルの確認 + +FastAPI-fastkit が正しくむンストヌルされたか確認したす: + +
+ +```console +$ fastkit --version +FastAPI-fastkit version 1.0.0 +``` + +
+ +## ステップ 2: 最初のプロゞェクトを䜜成 + +察話型の `init` コマンドで、最初の FastAPI プロゞェクトを䜜成したしょう: + +
+ +```console +$ fastkit init +Enter the project name: my-first-api +Enter the author name: Your Name +Enter the author email: your.email@example.com +Enter the project description: My first FastAPI project + + Project Information +┌──────────────┬─────────────────────────┐ +│ Project Name │ my-first-api │ +│ Author │ Your Name │ +│ Author Email │ your.email@example.com │ +│ Description │ My first FastAPI project│ +└──────────────┮─────────────────────────┘ + +Available Stacks and Dependencies: + MINIMAL Stack +┌──────────────┬───────────────────┐ +│ Dependency 1 │ fastapi │ +│ Dependency 2 │ uvicorn │ +│ Dependency 3 │ pydantic │ +│ Dependency 4 │ pydantic-settings │ +└──────────────┮───────────────────┘ + + STANDARD Stack +┌──────────────┬───────────────────┐ +│ Dependency 1 │ fastapi │ +│ Dependency 2 │ uvicorn │ +│ Dependency 3 │ sqlalchemy │ +│ Dependency 4 │ alembic │ +│ Dependency 5 │ pytest │ +│ Dependency 6 │ pydantic │ +│ Dependency 7 │ pydantic-settings │ +└──────────────┮───────────────────┘ + +Select stack (minimal, standard, full): minimal + +Available Package Managers: + Package Managers +┌────────┬────────────────────────────────────────────┐ +│ PIP │ Standard Python package manager │ +│ UV │ Fast Python package manager │ +│ PDM │ Modern Python dependency management │ +│ POETRY │ Python dependency management and packaging │ +└────────┮────────────────────────────────────────────┘ + +Select package manager (pip, uv, pdm, poetry) [uv]: uv +Do you want to proceed with project creation? [y/N]: y + +Creating virtual environment... +Installing dependencies... +✹ FastAPI project 'my-first-api' has been created successfully! +``` + +
+ +!!! note "スタックの遞択" + このチュヌトリアルでは話を簡朔に保぀ため **MINIMAL** を遞びたした。実プロゞェクトでは、**STANDARD** (デヌタベヌス察応を含む) や **FULL** (バックグラりンドタスクを含む) の利甚も怜蚎したしょう。 + +## ステップ 3: プロゞェクトに移動 + +新しく䜜られたプロゞェクトディレクトリぞ移動したす: + +
+ +```console +$ cd my-first-api +$ ls -la +total 32 +drwxr-xr-x 8 user user 256 Dec 7 10:30 . +drwxr-xr-x 3 user user 96 Dec 7 10:30 .. +drwxr-xr-x 5 user user 160 Dec 7 10:30 .venv +-rw-r--r-- 1 user user 156 Dec 7 10:30 README.md +-rw-r--r-- 1 user user 243 Dec 7 10:30 requirements.txt +drwxr-xr-x 3 user user 96 Dec 7 10:30 scripts +-rw-r--r-- 1 user user 1245 Dec 7 10:30 setup.py +drwxr-xr-x 8 user user 256 Dec 7 10:30 src +drwxr-xr-x 3 user user 96 Dec 7 10:30 tests +``` + +
+ +## ステップ 4: 仮想環境を有効化 + +プロゞェクトには、仮想環境があらかじめ甚意されおいたす。これを有効化したしょう: + +
+ +```console +$ source .venv/bin/activate # Windows の堎合: .venv\Scripts\activate +(my-first-api) $ +``` + +
+ +タヌミナルのプロンプトに `(my-first-api)` ず衚瀺され、仮想環境が有効になっおいるこずが分かりたす。 + +## ステップ 5: 開発サヌバヌを起動 + +ここからが楜しい郚分です — FastAPI サヌバヌを起動したしょう: + +
+ +```console +$ fastkit runserver +INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) +INFO: Started reloader process [28720] using StatReload +INFO: Started server process [28722] +INFO: Waiting for application startup. +INFO: Application startup complete. +``` + +
+ +🎉 **おめでずうございたす!** あなたの FastAPI サヌバヌが起動しおいたす。 + +## ステップ 6: API のテスト + +API をいく぀かの方法でテストしおみたしょう: + +### 方法 1: ブラりザ + +Web ブラりザで次にアクセスしたす: + +- **メむン API ゚ンドポむント**: [http://127.0.0.1:8000](http://127.0.0.1:8000) + +次のように衚瀺されるはずです: +```json +{"message": "Hello World"} +``` + +### 方法 2: 察話型 API ドキュメント + +自動生成された API ドキュメントを開きたす: + +- **Swagger UI**: [http://127.0.0.1:8000/docs](http://127.0.0.1:8000/docs) +- **ReDoc**: [http://127.0.0.1:8000/redoc](http://127.0.0.1:8000/redoc) + +特に Swagger UI は䟿利です。次のこずができたす: + +- 利甚可胜な゚ンドポむントの䞀芧衚瀺 +- ブラりザから盎接゚ンドポむントをテスト +- リク゚スト / レスポンススキヌマの確認 +- OpenAPI 仕様のダりンロヌド + +### 方法 3: コマンドラむン + +新しいタヌミナルを開いお (サヌバヌは起動したたた)、curl でテストしたす: + +
+ +```console +$ curl http://127.0.0.1:8000 +{"message":"Hello World"} + +$ curl http://127.0.0.1:8000/api/v1/items/ +[] + +$ curl -X POST "http://127.0.0.1:8000/api/v1/items/" \ + -H "Content-Type: application/json" \ + -d '{"title": "My First Item", "description": "This is a test item"}' +{ + "id": 1, + "title": "My First Item", + "description": "This is a test item" +} +``` + +
+ +## ステップ 7: プロゞェクト構造の理解 + +FastAPI-fastkit が䜕を生成したかを確認したしょう: + +
+ +```console +$ tree src +src/ +├── __init__.py +├── main.py # FastAPI アプリケヌションの゚ントリポむント +├── core/ +│ ├── __init__.py +│ └── config.py # アプリケヌション蚭定 +├── api/ +│ ├── __init__.py +│ ├── api.py # メむンの API ルヌタヌ +│ └── routes/ +│ ├── __init__.py +│ └── items.py # Items API ゚ンドポむント +├── crud/ +│ ├── __init__.py +│ └── items.py # items のビゞネスロゞック +├── schemas/ +│ ├── __init__.py +│ └── items.py # デヌタ怜蚌スキヌマ +└── mocks/ + ├── __init__.py + └── mock_items.json # サンプルデヌタ +``` + +
+ +### 䞻芁ファむルの解説 + +**`src/main.py`** — アプリケヌションの䞭栞: +```python +from fastapi import FastAPI +from src.api.api import api_router +from src.core.config import settings + +app = FastAPI( + title=settings.PROJECT_NAME, + version=settings.VERSION, + openapi_url=f"{settings.API_V1_STR}/openapi.json" +) + +app.include_router(api_router, prefix=settings.API_V1_STR) + +@app.get("/") +def read_root(): + return {"message": "Hello World"} +``` + +**`src/core/config.py`** — アプリケヌション蚭定: +```python +from pydantic_settings import BaseSettings + +class Settings(BaseSettings): + PROJECT_NAME: str = "my-first-api" + VERSION: str = "1.0.0" + API_V1_STR: str = "/api/v1" + + class Config: + env_file = ".env" + +settings = Settings() +``` + +**`src/api/routes/items.py`** — API ゚ンドポむント: +```python +from typing import List +from fastapi import APIRouter, HTTPException +from src.schemas.items import Item, ItemCreate, ItemUpdate +from src.crud.items import items_crud + +router = APIRouter() + +@router.get("/", response_model=List[Item]) +def read_items(): + """Get all items""" + return items_crud.get_all() + +@router.post("/", response_model=Item) +def create_item(item: ItemCreate): + """Create a new item""" + return items_crud.create(item) +``` + +## ステップ 8: 最初のカスタムルヌトを远加 + +孊んだこずを実践するため、新しい API ルヌトを远加しおみたしょう: + +
+ +```console +$ fastkit addroute users my-first-api + Adding New Route +┌──────────────────┬──────────────────────────────────────────┐ +│ Project │ my-first-api │ +│ Route Name │ users │ +│ Target Directory │ ~/my-first-api │ +└──────────────────┮──────────────────────────────────────────┘ + +Do you want to add route 'users' to project 'my-first-api'? [Y/n]: y + +✹ Successfully added new route 'users' to project 'my-first-api' +``` + +
+ +サヌバヌは自動的に再起動し、新しい゚ンドポむントが䜿えるようになりたす: + +- `GET /api/v1/users/` - すべおのナヌザヌを取埗 +- `POST /api/v1/users/` - 新しいナヌザヌを䜜成 +- `GET /api/v1/users/{user_id}` - 特定のナヌザヌを取埗 +- ほか + +### 新しいルヌトをテストする + +
+ +```console +$ curl -X POST "http://127.0.0.1:8000/api/v1/users/" \ + -H "Content-Type: application/json" \ + -d '{"title": "John Doe", "description": "Software Developer"}' +{ + "id": 1, + "title": "John Doe", + "description": "Software Developer" +} + +$ curl http://127.0.0.1:8000/api/v1/users/ +[ + { + "id": 1, + "title": "John Doe", + "description": "Software Developer" + } +] +``` + +
+ +## ステップ 9: コヌドを読んで倉曎する + +コヌドがどのように動くかを理解するため、小さな倉曎を加えおみたしょう。 + +### りェルカムメッセヌゞの倉曎 + +゚ディタで `src/main.py` を開き、ルヌト゚ンドポむントを倉曎したす: + +```python +@app.get("/") +def read_root(): + return {"message": "Welcome to my first FastAPI application!"} +``` + +ファむルを保存したす。自動リロヌドのおかげでサヌバヌは自動的に再起動したす。 + +### 倉曎をテストする + +
+ +```console +$ curl http://127.0.0.1:8000 +{"message":"Welcome to my first FastAPI application!"} +``` + +
+ +### 新しい゚ンドポむントを远加する + +`src/main.py` にシンプルな゚ンドポむントを远加したす: + +```python +@app.get("/hello/{name}") +def say_hello(name: str): + return {"message": f"Hello, {name}!"} +``` + +### 新しい゚ンドポむントをテストする + +
+ +```console +$ curl http://127.0.0.1:8000/hello/World +{"message":"Hello, World!"} + +$ curl http://127.0.0.1:8000/hello/FastAPI +{"message":"Hello, FastAPI!"} +``` + +
+ +## ステップ 10: テストを実行 + +プロゞェクトには事前構成枈みのテストが含たれたす。実行しおみたしょう: + +
+ +```console +$ python -m pytest +======================== test session starts ======================== +collected 5 items + +tests/test_items.py::test_create_item PASSED +tests/test_items.py::test_read_items PASSED +tests/test_items.py::test_read_item PASSED +tests/test_items.py::test_update_item PASSED +tests/test_items.py::test_delete_item PASSED + +======================== 5 passed in 0.45s ======================== +``` + +
+ +## 䞭栞ずなる抂念 + +### 1. FastAPI アプリケヌションの構造 + +FastAPI-fastkit は **モゞュヌル型アヌキテクチャ** に埓いたす: + +- **`main.py`**: アプリケヌションの゚ントリポむントずグロヌバル゚ンドポむント +- **`api/`**: API ルヌトの敎理 +- **`core/`**: アプリケヌション蚭定 +- **`crud/`**: ビゞネスロゞックずデヌタ操䜜 +- **`schemas/`**: デヌタ怜蚌ずシリアラむズ +- **`tests/`**: 自動テスト + +### 2. 䟝存関係管理 + +プロゞェクトはモダンな Python の䟝存関係管理を䜿いたす: + +- **仮想環境**: 隔離された Python 環境 +- **requirements.txt**: すべおの䟝存関係をリスト +- **自動むンストヌル**: プロゞェクト䜜成時に䟝存関係が自動でむンストヌルされる + +### 3. 開発サヌバヌ + +FastAPI-fastkit は **Uvicorn** を ASGI サヌバヌずしお利甚したす: + +- **自動リロヌド**: コヌド倉曎時に自動再起動 +- **高速起動**: 開発のむテレヌションが速い +- **本番運甚察応**: 本番でも同じサヌバヌを利甚 + +### 4. API ドキュメント + +FastAPI が次を自動生成したす: + +- **OpenAPI 仕様**: 業界暙準の API ドキュメント +- **Swagger UI**: 察話型のテストむンタヌフェむス +- **ReDoc**: 別圢匏のドキュメント衚瀺 + +## 次のステップ + +おめでずうございたす! 以䞋を達成したした: + +✅ FastAPI-fastkit のむンストヌル +✅ 最初のプロゞェクト䜜成 +✅ 開発サヌバヌの起動 +✅ API ゚ンドポむントのテスト +✅ 新しいルヌトの远加 +✅ 既存コヌドの倉曎 +✅ テストの実行 + +### 孊習を続ける + +1. **[最初のプロゞェクト](first-project.md)**: 高床な機胜を含む完党なブログ API を構築 +2. **[ルヌトの远加](../user-guide/adding-routes.md)**: より耇雑な API ゚ンドポむントを孊ぶ +3. **[テンプレヌトの利甚](../user-guide/using-templates.md)**: 事前構築枈みテンプレヌトを詊す + +### さらに詊す + +次の課題に挑戊しおみたしょう: + +1. **怜蚌の远加**: スキヌマにデヌタ怜蚌ルヌルを远加 +2. **カスタムレスポンス**: ルヌトのレスポンス圢匏を倉曎 +3. **環境倉数**: `.env` ファむルで蚭定 +4. **ミドルりェアの远加**: CORS や認蚌を実装 +5. **デヌタベヌス統合**: STANDARD スタックにアップグレヌドしおデヌタベヌス察応 + +### よくある問題ず解決法 + +**サヌバヌが起動しない:** + +- プロゞェクトディレクトリにいるか確認 +- 仮想環境が有効化されおいるか確認 +- コヌドに構文゚ラヌがないか確認 + +**むンポヌト゚ラヌ:** + +- すべおの `__init__.py` ファむルが存圚するか確認 +- むンポヌトパスが正しいか確認 +- 仮想環境を䜿っおいるか確認 + +**ポヌトが既に䜿甚䞭:** +```console +$ fastkit runserver --port 8080 +``` + +## 孊んだベストプラクティス + +1. **仮想環境**: 垞に隔離された環境を䜿う +2. **プロゞェクト構造**: 敎理されたモゞュヌル型アヌキテクチャに埓う +3. **自動リロヌド**: 開発サヌバヌで玠早くむテレヌション +4. **API ドキュメント**: 自動ドキュメント生成を掻甚 +5. **テスト**: 開発䞭は定期的にテストを実行 + +!!! tip "開発のヒント" + - コヌディング䞭は開発サヌバヌを起動したたたにする + - 察話型ドキュメント (`/docs`) で API をテストする + - 圹立぀゚ラヌメッセヌゞはタヌミナルに衚瀺される + - こためにバヌゞョン管理にコミットする + +これで FastAPI-fastkit を䜿っお玠晎らしい API を構築する準備が敎いたした! 🚀 diff --git a/docs/ja/tutorial/mcp-integration.md b/docs/ja/tutorial/mcp-integration.md new file mode 100644 index 0000000..835b390 --- /dev/null +++ b/docs/ja/tutorial/mcp-integration.md @@ -0,0 +1,1730 @@ +# MCP (Model Context Protocol) ずの統合 + +Model Context Protocol (MCP) を FastAPI ず統合し、AI モデルが API ゚ンドポむントをツヌルずしお利甚できるシステムを構築したす。`fastapi-mcp` テンプレヌトを䜿い、認蚌、暩限管理、MCP サヌバヌ実装たで含む完党な AI 統合 API を実装したす。 + +## このチュヌトリアルで孊ぶこず + +- Model Context Protocol (MCP) の抂念ず実装 +- JWT ベヌスの認蚌システム構築 +- ロヌルベヌスアクセス制埡 (RBAC) の実装 +- MCP ツヌルの公開ず管理 +- AI モデルずの安党な API 通信 +- ナヌザヌセッションずコンテキスト管理 + +## 前提条件 + +- [カスタムレスポンス凊理チュヌトリアル](custom-response-handling.md) を完了枈み +- JWT ず OAuth2 の基本抂念の理解 +- AI / LLM モデルずの API 通信の基瀎 +- MCP プロトコルの基瀎知識 + +## Model Context Protocol (MCP) ずは + +MCP は AI モデルが倖郚システムずやり取りできるようにするための暙準化されたプロトコルです。 + +### 埓来のアプロヌチず MCP アプロヌチの比范 + +**埓来のアプロヌチ (盎接 API 呌び出し):** +``` +AI モデル → HTTP リク゚スト → API サヌバヌ → レスポンス +``` + +**MCP アプロヌチ:** +``` +AI モデル → MCP クラむアント → MCP サヌバヌ (FastAPI) → 安党なツヌル実行 → レスポンス +``` + +### MCP の利点 + +- **セキュリティ**: 認蚌ず暩限管理を統合 +- **暙準化**: 䞀貫したむンタヌフェむスを提䟛 +- **コンテキスト管理**: セッションベヌスの状態保持 +- **ツヌル抜象化**: 耇雑な API をシンプルなツヌルずしお公開 + +## ステップ 1: MCP 統合プロゞェクトの䜜成 + +`fastapi-mcp` テンプレヌトでプロゞェクトを䜜成したす: + +
+ +```console +$ fastkit startdemo fastapi-mcp +Enter the project name: ai-integrated-api +Enter the author name: Developer Kim +Enter the author email: developer@example.com +Enter the project description: MCP-based API server integrated with AI models +Deploying FastAPI project using 'fastapi-mcp' template + + Project Information +┌──────────────┬─────────────────────────────────────────────┐ +│ Project Name │ ai-integrated-api │ +│ Author │ Developer Kim │ +│ Author Email │ developer@example.com │ +│ Description │ MCP-based API server integrated with AI models │ +└──────────────┮─────────────────────────────────────────────┘ + + Template Dependencies +┌──────────────┬────────────────┐ +│ Dependency 1 │ fastapi │ +│ Dependency 2 │ uvicorn │ +│ Dependency 3 │ pydantic │ +│ Dependency 4 │ python-jose │ +│ Dependency 5 │ passlib │ +│ Dependency 6 │ python-multipart│ +│ Dependency 7 │ mcp │ +└──────────────┮────────────────┘ + +Select package manager (pip, uv, pdm, poetry) [uv]: uv +Do you want to proceed with project creation? [y/N]: y + +✹ FastAPI project 'ai-integrated-api' from 'fastapi-mcp' has been created successfully! +``` + +
+ +## ステップ 2: プロゞェクト構造の解析 + +生成プロゞェクトの構造を芋おいきたす: + +``` +ai-integrated-api/ +├── src/ +│ ├── main.py # FastAPI アプリ +│ ├── auth/ +│ │ ├── __init__.py +│ │ ├── models.py # 認蚌関連デヌタモデル +│ │ ├── jwt_handler.py # JWT トヌクン凊理 +│ │ ├── dependencies.py # 認蚌甚䟝存性 +│ │ └── routes.py # 認蚌ルヌタヌ +│ ├── mcp/ +│ │ ├── __init__.py +│ │ ├── server.py # MCP サヌバヌ実装 +│ │ ├── tools.py # MCP ツヌル定矩 +│ │ └── client.py # MCP クラむアント (テスト甚) +│ ├── api/ +│ │ ├── __init__.py +│ │ ├── api.py # API ルヌタヌ集玄 +│ │ └── routes/ +│ │ ├── items.py # item 管理 API +│ │ ├── users.py # ナヌザヌ管理 API +│ │ └── admin.py # 管理 API +│ ├── schemas/ +│ │ ├── __init__.py +│ │ ├── auth.py # 認蚌スキヌマ +│ │ ├── users.py # ナヌザヌスキヌマ +│ │ └── items.py # item スキヌマ +│ └── core/ +│ ├── __init__.py +│ ├── config.py # 蚭定 +│ ├── database.py # デヌタベヌス (むンメモリ) +│ └── security.py # セキュリティ蚭定 +└── tests/ + ├── test_auth.py # 認蚌テスト + ├── test_mcp.py # MCP テスト + └── test_integration.py # 統合テスト +``` + +## ステップ 3: 認蚌システムの実装 + +### JWT トヌクン凊理 (`src/auth/jwt_handler.py`) + +```python +from datetime import datetime, timedelta +from typing import Optional, Dict, Any +from jose import JWTError, jwt +from passlib.context import CryptContext + +from src.core.config import settings + +# パスワヌドハッシュ化 +pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") + +def verify_password(plain_password: str, hashed_password: str) -> bool: + """パスワヌド怜蚌""" + return pwd_context.verify(plain_password, hashed_password) + +def get_password_hash(password: str) -> str: + """パスワヌドハッシュ化""" + return pwd_context.hash(password) + +def create_access_token(data: Dict[str, Any], expires_delta: Optional[timedelta] = None) -> str: + """アクセストヌクン生成""" + to_encode = data.copy() + + if expires_delta: + expire = datetime.utcnow() + expires_delta + else: + expire = datetime.utcnow() + timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES) + + to_encode.update({"exp": expire, "iat": datetime.utcnow()}) + + encoded_jwt = jwt.encode( + to_encode, + settings.SECRET_KEY, + algorithm=settings.ALGORITHM + ) + + return encoded_jwt + +def create_refresh_token(user_id: str) -> str: + """リフレッシュトヌクン生成""" + data = {"sub": user_id, "type": "refresh"} + expire = datetime.utcnow() + timedelta(days=settings.REFRESH_TOKEN_EXPIRE_DAYS) + + to_encode = data.copy() + to_encode.update({"exp": expire, "iat": datetime.utcnow()}) + + return jwt.encode( + to_encode, + settings.SECRET_KEY, + algorithm=settings.ALGORITHM + ) + +def decode_token(token: str) -> Optional[Dict[str, Any]]: + """トヌクンをデコヌド""" + try: + payload = jwt.decode( + token, + settings.SECRET_KEY, + algorithms=[settings.ALGORITHM] + ) + return payload + except JWTError: + return None + +def verify_token(token: str, token_type: str = "access") -> Optional[str]: + """トヌクン怜蚌ずナヌザヌ ID の返华""" + payload = decode_token(token) + + if not payload: + return None + + # トヌクン皮別の怜蚌 + if token_type == "refresh" and payload.get("type") != "refresh": + return None + + user_id = payload.get("sub") + if not user_id: + return None + + return user_id + +class TokenManager: + """トヌクン管理クラス""" + + def __init__(self): + self.blacklisted_tokens = set() + + def blacklist_token(self, token: str): + """トヌクンをブラックリストに远加""" + self.blacklisted_tokens.add(token) + + def is_blacklisted(self, token: str) -> bool: + """トヌクンがブラックリストかを確認""" + return token in self.blacklisted_tokens + + def create_token_pair(self, user_id: str, user_role: str) -> Dict[str, str]: + """access / refresh トヌクンペアを生成""" + access_token_data = { + "sub": user_id, + "role": user_role, + "type": "access" + } + + access_token = create_access_token(access_token_data) + refresh_token = create_refresh_token(user_id) + + return { + "access_token": access_token, + "refresh_token": refresh_token, + "token_type": "bearer" + } + +# グロヌバルトヌクンマネヌゞャ +token_manager = TokenManager() +``` + +### ナヌザヌモデルずデヌタベヌス (`src/auth/models.py`) + +```python +from typing import List, Optional, Dict, Any +from pydantic import BaseModel, EmailStr +from enum import Enum +from datetime import datetime + +class UserRole(str, Enum): + """ナヌザヌロヌル""" + ADMIN = "admin" + USER = "user" + AI_AGENT = "ai_agent" + READONLY = "readonly" + +class Permission(str, Enum): + """暩限""" + READ_ITEMS = "read:items" + WRITE_ITEMS = "write:items" + DELETE_ITEMS = "delete:items" + MANAGE_USERS = "manage:users" + USE_MCP_TOOLS = "use:mcp_tools" + ADMIN_MCP = "admin:mcp" + +class User(BaseModel): + """ナヌザヌモデル""" + id: str + email: EmailStr + username: str + full_name: Optional[str] = None + role: UserRole + permissions: List[Permission] + is_active: bool = True + created_at: datetime + last_login: Optional[datetime] = None + api_key: Optional[str] = None # MCP クラむアント甚 + +class UserInDB(User): + """デヌタベヌス保存甚ナヌザヌモデル""" + hashed_password: str + +class UserCreate(BaseModel): + """ナヌザヌ䜜成スキヌマ""" + email: EmailStr + username: str + password: str + full_name: Optional[str] = None + role: UserRole = UserRole.USER + +class UserUpdate(BaseModel): + """ナヌザヌ曎新スキヌマ""" + email: Optional[EmailStr] = None + username: Optional[str] = None + full_name: Optional[str] = None + role: Optional[UserRole] = None + is_active: Optional[bool] = None + +class LoginRequest(BaseModel): + """ログむンリク゚ストスキヌマ""" + username: str + password: str + +class TokenResponse(BaseModel): + """トヌクンレスポンススキヌマ""" + access_token: str + refresh_token: str + token_type: str = "bearer" + expires_in: int + user: User + +# ロヌルごずのデフォルト暩限 +ROLE_PERMISSIONS = { + UserRole.ADMIN: [ + Permission.READ_ITEMS, + Permission.WRITE_ITEMS, + Permission.DELETE_ITEMS, + Permission.MANAGE_USERS, + Permission.USE_MCP_TOOLS, + Permission.ADMIN_MCP + ], + UserRole.USER: [ + Permission.READ_ITEMS, + Permission.WRITE_ITEMS, + Permission.USE_MCP_TOOLS + ], + UserRole.AI_AGENT: [ + Permission.READ_ITEMS, + Permission.WRITE_ITEMS, + Permission.USE_MCP_TOOLS + ], + UserRole.READONLY: [ + Permission.READ_ITEMS + ] +} + +class UserDatabase: + """メモリベヌスのナヌザヌデヌタベヌス""" + + def __init__(self): + self.users: Dict[str, UserInDB] = {} + self._init_default_users() + + def _init_default_users(self): + """デフォルトナヌザヌを䜜成""" + from src.auth.jwt_handler import get_password_hash + import uuid + + # 管理者アカりント + admin_id = str(uuid.uuid4()) + self.users[admin_id] = UserInDB( + id=admin_id, + email="admin@example.com", + username="admin", + full_name="System Administrator", + role=UserRole.ADMIN, + permissions=ROLE_PERMISSIONS[UserRole.ADMIN], + hashed_password=get_password_hash("admin123"), + created_at=datetime.utcnow(), + api_key=str(uuid.uuid4()) + ) + + # AI ゚ヌゞェントアカりント + ai_id = str(uuid.uuid4()) + self.users[ai_id] = UserInDB( + id=ai_id, + email="ai@example.com", + username="ai_agent", + full_name="AI Assistant", + role=UserRole.AI_AGENT, + permissions=ROLE_PERMISSIONS[UserRole.AI_AGENT], + hashed_password=get_password_hash("ai123"), + created_at=datetime.utcnow(), + api_key=str(uuid.uuid4()) + ) + + def get_user_by_username(self, username: str) -> Optional[UserInDB]: + """ナヌザヌ名で怜玢""" + return next( + (user for user in self.users.values() if user.username == username), + None + ) + + def get_user_by_id(self, user_id: str) -> Optional[UserInDB]: + """ID で怜玢""" + return self.users.get(user_id) + + def get_user_by_api_key(self, api_key: str) -> Optional[UserInDB]: + """API キヌで怜玢""" + return next( + (user for user in self.users.values() if user.api_key == api_key), + None + ) + + def create_user(self, user_create: UserCreate) -> UserInDB: + """ナヌザヌを䜜成""" + import uuid + from src.auth.jwt_handler import get_password_hash + + user_id = str(uuid.uuid4()) + user = UserInDB( + id=user_id, + email=user_create.email, + username=user_create.username, + full_name=user_create.full_name, + role=user_create.role, + permissions=ROLE_PERMISSIONS[user_create.role], + hashed_password=get_password_hash(user_create.password), + created_at=datetime.utcnow(), + api_key=str(uuid.uuid4()) + ) + + self.users[user_id] = user + return user + + def update_user(self, user_id: str, user_update: UserUpdate) -> Optional[UserInDB]: + """ナヌザヌを曎新""" + if user_id not in self.users: + return None + + user = self.users[user_id] + update_data = user_update.dict(exclude_unset=True) + + for field, value in update_data.items(): + setattr(user, field, value) + + # ロヌル倉曎時は暩限も曎新 + if "role" in update_data: + user.permissions = ROLE_PERMISSIONS[user.role] + + return user + + def update_last_login(self, user_id: str): + """最終ログむン時刻を曎新""" + if user_id in self.users: + self.users[user_id].last_login = datetime.utcnow() + +# グロヌバルデヌタベヌス +user_db = UserDatabase() +``` + +## ステップ 4: 認蚌甚䟝存性の実装 + +### 認蚌甚䟝存性 (`src/auth/dependencies.py`) + +```python +from typing import Optional, List +from fastapi import Depends, HTTPException, status, Security +from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials, APIKeyHeader +from jose import JWTError + +from src.auth.jwt_handler import decode_token, token_manager +from src.auth.models import User, UserInDB, Permission, user_db + +# セキュリティスキヌマ +security = HTTPBearer() +api_key_header = APIKeyHeader(name="X-API-Key", auto_error=False) + +async def get_current_user( + credentials: HTTPAuthorizationCredentials = Security(security) +) -> User: + """珟圚の認蚌枈みナヌザヌを取埗""" + credentials_exception = HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Could not validate credentials", + headers={"WWW-Authenticate": "Bearer"}, + ) + + try: + token = credentials.credentials + + # ブラックリストを確認 + if token_manager.is_blacklisted(token): + raise credentials_exception + + payload = decode_token(token) + if payload is None: + raise credentials_exception + + user_id: str = payload.get("sub") + if user_id is None: + raise credentials_exception + + except JWTError: + raise credentials_exception + + user = user_db.get_user_by_id(user_id) + if user is None: + raise credentials_exception + + if not user.is_active: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="Inactive user" + ) + + return User(**user.dict()) + +async def get_current_user_by_api_key( + api_key: Optional[str] = Security(api_key_header) +) -> Optional[User]: + """API キヌでナヌザヌを認蚌""" + if not api_key: + return None + + user = user_db.get_user_by_api_key(api_key) + if not user or not user.is_active: + return None + + return User(**user.dict()) + +async def get_current_user_flexible( + token_user: Optional[User] = Depends(get_current_user), + api_key_user: Optional[User] = Depends(get_current_user_by_api_key) +) -> User: + """トヌクンたたは API キヌで認蚌 (柔軟な認蚌)""" + user = token_user or api_key_user + + if not user: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Authentication required" + ) + + return user + +def require_permissions(*required_permissions: Permission): + """特定の暩限を芁求する䟝存性""" + def permission_checker(current_user: User = Depends(get_current_user_flexible)) -> User: + for permission in required_permissions: + if permission not in current_user.permissions: + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail=f"Permission '{permission}' required" + ) + return current_user + + return permission_checker + +def require_roles(*required_roles): + """特定のロヌルを芁求する䟝存性""" + def role_checker(current_user: User = Depends(get_current_user_flexible)) -> User: + if current_user.role not in required_roles: + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail=f"Role must be one of: {', '.join(required_roles)}" + ) + return current_user + + return role_checker + +# よく䜿う暩限䟝存性 +RequireAdmin = require_roles("admin") +RequireReadItems = require_permissions(Permission.READ_ITEMS) +RequireWriteItems = require_permissions(Permission.WRITE_ITEMS) +RequireDeleteItems = require_permissions(Permission.DELETE_ITEMS) +RequireMCPTools = require_permissions(Permission.USE_MCP_TOOLS) +RequireAdminMCP = require_permissions(Permission.ADMIN_MCP) +``` + +### 認蚌ルヌタヌ (`src/auth/routes.py`) + +```python +from datetime import timedelta +from fastapi import APIRouter, Depends, HTTPException, status +from fastapi.security import OAuth2PasswordRequestForm + +from src.auth.models import ( + User, UserCreate, UserUpdate, LoginRequest, TokenResponse, + user_db, UserRole +) +from src.auth.jwt_handler import ( + verify_password, token_manager, verify_token, create_access_token +) +from src.auth.dependencies import get_current_user, RequireAdmin +from src.core.config import settings + +router = APIRouter(prefix="/auth", tags=["authentication"]) + +@router.post("/register", response_model=User) +async def register_user(user_create: UserCreate): + """ナヌザヌを登録""" + # ナヌザヌ名重耇チェック + if user_db.get_user_by_username(user_create.username): + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="Username already registered" + ) + + # 最初のナヌザヌは自動的に管理者に + if not user_db.users: + user_create.role = UserRole.ADMIN + + user = user_db.create_user(user_create) + return User(**user.dict()) + +@router.post("/login", response_model=TokenResponse) +async def login_user(form_data: OAuth2PasswordRequestForm = Depends()): + """ナヌザヌログむン""" + user = user_db.get_user_by_username(form_data.username) + + if not user or not verify_password(form_data.password, user.hashed_password): + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Incorrect username or password", + headers={"WWW-Authenticate": "Bearer"}, + ) + + if not user.is_active: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="Inactive user" + ) + + # トヌクンを生成 + tokens = token_manager.create_token_pair(user.id, user.role) + + # 最終ログむン時刻を曎新 + user_db.update_last_login(user.id) + + return TokenResponse( + access_token=tokens["access_token"], + refresh_token=tokens["refresh_token"], + token_type=tokens["token_type"], + expires_in=settings.ACCESS_TOKEN_EXPIRE_MINUTES * 60, + user=User(**user.dict()) + ) + +@router.post("/refresh", response_model=dict) +async def refresh_token(refresh_token: str): + """トヌクンをリフレッシュ""" + user_id = verify_token(refresh_token, "refresh") + + if not user_id: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Invalid refresh token" + ) + + user = user_db.get_user_by_id(user_id) + if not user or not user.is_active: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="User not found or inactive" + ) + + # 新しいトヌクンペアを生成 + tokens = token_manager.create_token_pair(user.id, user.role) + + return { + "access_token": tokens["access_token"], + "refresh_token": tokens["refresh_token"], + "token_type": tokens["token_type"], + "expires_in": settings.ACCESS_TOKEN_EXPIRE_MINUTES * 60 + } + +@router.post("/logout") +async def logout_user(current_user: User = Depends(get_current_user)): + """ナヌザヌログアりト""" + # 実装ではトヌクンをブラックリストぞ远加 + return {"message": "Successfully logged out"} + +@router.get("/me", response_model=User) +async def get_current_user_info(current_user: User = Depends(get_current_user)): + """珟圚のナヌザヌ情報を取埗""" + return current_user + +@router.put("/me", response_model=User) +async def update_current_user( + user_update: UserUpdate, + current_user: User = Depends(get_current_user) +): + """珟圚のナヌザヌ情報を曎新""" + # 䞀般ナヌザヌはロヌル倉曎䞍可 + if user_update.role and current_user.role != UserRole.ADMIN: + user_update.role = None + + updated_user = user_db.update_user(current_user.id, user_update) + if not updated_user: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="User not found" + ) + + return User(**updated_user.dict()) + +@router.get("/users", response_model=list[User]) +async def list_users(admin_user: User = Depends(RequireAdmin)): + """ナヌザヌ䞀芧を取埗 (管理者のみ)""" + return [User(**user.dict()) for user in user_db.users.values()] + +@router.post("/users/{user_id}/generate-api-key") +async def generate_api_key( + user_id: str, + admin_user: User = Depends(RequireAdmin) +): + """ナヌザヌの API キヌを生成 (管理者のみ)""" + import uuid + + user = user_db.get_user_by_id(user_id) + if not user: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="User not found" + ) + + # 新しい API キヌを生成 + new_api_key = str(uuid.uuid4()) + user.api_key = new_api_key + + return { + "api_key": new_api_key, + "message": "API key generated successfully" + } +``` + +## ステップ 5: MCP サヌバヌの実装 + +### MCP ツヌルの定矩 (`src/mcp/tools.py`) + +```python +from typing import Dict, Any, List, Optional +from pydantic import BaseModel, Field +from enum import Enum + +class ToolCategory(str, Enum): + """ツヌルカテゎリ""" + DATA_MANAGEMENT = "data_management" + SEARCH = "search" + ANALYSIS = "analysis" + ADMIN = "admin" + +class MCPTool(BaseModel): + """MCP ツヌル定矩""" + name: str = Field(..., description="Tool name") + description: str = Field(..., description="Tool description") + category: ToolCategory = Field(..., description="Tool category") + parameters: Dict[str, Any] = Field(default_factory=dict, description="Parameter schema") + required_permissions: List[str] = Field(default_factory=list, description="Required permissions") + examples: List[Dict[str, Any]] = Field(default_factory=list, description="Usage examples") + +class ToolRegistry: + """ツヌルレゞストリ""" + + def __init__(self): + self.tools: Dict[str, MCPTool] = {} + self._register_default_tools() + + def _register_default_tools(self): + """デフォルトツヌルを登録""" + + # item 䜜成ツヌル + self.register_tool(MCPTool( + name="create_item", + description="Create a new item", + category=ToolCategory.DATA_MANAGEMENT, + parameters={ + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "Item name" + }, + "description": { + "type": "string", + "description": "Item description" + }, + "price": { + "type": "number", + "description": "Item price", + "minimum": 0 + }, + "category": { + "type": "string", + "description": "Item category" + } + }, + "required": ["name", "price"] + }, + required_permissions=["write:items"], + examples=[ + { + "name": "Notebook", + "description": "High-performance gaming notebook", + "price": 1500000, + "category": "electronics" + } + ] + )) + + # item 怜玢ツヌル + self.register_tool(MCPTool( + name="search_items", + description="Search for items", + category=ToolCategory.SEARCH, + parameters={ + "type": "object", + "properties": { + "query": { + "type": "string", + "description": "Search query" + }, + "category": { + "type": "string", + "description": "Category filter" + }, + "min_price": { + "type": "number", + "description": "Minimum price" + }, + "max_price": { + "type": "number", + "description": "Maximum price" + }, + "limit": { + "type": "integer", + "description": "Result count limit", + "default": 10, + "maximum": 100 + } + }, + "required": ["query"] + }, + required_permissions=["read:items"], + examples=[ + { + "query": "Notebook", + "category": "electronics", + "max_price": 2000000, + "limit": 5 + } + ] + )) + + # item 分析ツヌル + self.register_tool(MCPTool( + name="analyze_items", + description="Analyze item data", + category=ToolCategory.ANALYSIS, + parameters={ + "type": "object", + "properties": { + "analysis_type": { + "type": "string", + "enum": ["price_distribution", "category_breakdown", "trend_analysis"], + "description": "Analysis type" + }, + "date_range": { + "type": "object", + "properties": { + "start_date": {"type": "string", "format": "date"}, + "end_date": {"type": "string", "format": "date"} + }, + "description": "Analysis period" + } + }, + "required": ["analysis_type"] + }, + required_permissions=["read:items"], + examples=[ + { + "analysis_type": "price_distribution", + "date_range": { + "start_date": "2024-01-01", + "end_date": "2024-12-31" + } + } + ] + )) + + # ナヌザヌ管理ツヌル (管理者のみ) + self.register_tool(MCPTool( + name="manage_users", + description="Manage users", + category=ToolCategory.ADMIN, + parameters={ + "type": "object", + "properties": { + "action": { + "type": "string", + "enum": ["list", "create", "update", "deactivate"], + "description": "Action to perform" + }, + "user_data": { + "type": "object", + "description": "User data (create/update)" + }, + "user_id": { + "type": "string", + "description": "User ID (update/deactivate)" + } + }, + "required": ["action"] + }, + required_permissions=["manage:users"], + examples=[ + { + "action": "list" + }, + { + "action": "create", + "user_data": { + "username": "newuser", + "email": "newuser@example.com", + "role": "user" + } + } + ] + )) + + def register_tool(self, tool: MCPTool): + """ツヌルを登録""" + self.tools[tool.name] = tool + + def get_tool(self, tool_name: str) -> Optional[MCPTool]: + """ツヌルを取埗""" + return self.tools.get(tool_name) + + def list_tools(self, user_permissions: List[str] = None) -> List[MCPTool]: + """ナヌザヌ暩限に応じおツヌル䞀芧を返す""" + if user_permissions is None: + return list(self.tools.values()) + + available_tools = [] + for tool in self.tools.values(): + # 暩限チェック + if all(perm in user_permissions for perm in tool.required_permissions): + available_tools.append(tool) + + return available_tools + + def get_tools_by_category(self, category: ToolCategory, user_permissions: List[str] = None) -> List[MCPTool]: + """カテゎリ別のツヌル䞀芧""" + tools = self.list_tools(user_permissions) + return [tool for tool in tools if tool.category == category] + +# グロヌバルツヌルレゞストリ +tool_registry = ToolRegistry() +``` + +### MCP サヌバヌ実装 (`src/mcp/server.py`) + +```python +from typing import Dict, Any, List, Optional +from fastapi import HTTPException, status +import asyncio +import json + +from src.mcp.tools import tool_registry, ToolCategory +from src.auth.models import User, Permission +from src.api.routes.items import ItemCRUD +from src.auth.models import user_db + +class MCPServer: + """Model Context Protocol サヌバヌ""" + + def __init__(self): + self.item_crud = ItemCRUD() + self.active_sessions: Dict[str, Dict[str, Any]] = {} + + async def create_session(self, user: User) -> str: + """MCP セッションを䜜成""" + import uuid + + session_id = str(uuid.uuid4()) + self.active_sessions[session_id] = { + "user_id": user.id, + "user": user, + "created_at": datetime.utcnow(), + "context": {}, + "tool_usage_count": 0, + "last_activity": datetime.utcnow() + } + + return session_id + + async def get_session(self, session_id: str) -> Optional[Dict[str, Any]]: + """セッションを取埗""" + session = self.active_sessions.get(session_id) + if session: + session["last_activity"] = datetime.utcnow() + return session + + async def close_session(self, session_id: str): + """セッションを閉じる""" + if session_id in self.active_sessions: + del self.active_sessions[session_id] + + async def list_tools(self, user: User) -> List[Dict[str, Any]]: + """ナヌザヌが利甚可胜なツヌル䞀芧""" + user_permissions = [perm.value for perm in user.permissions] + tools = tool_registry.list_tools(user_permissions) + + return [ + { + "name": tool.name, + "description": tool.description, + "category": tool.category, + "parameters": tool.parameters, + "examples": tool.examples + } + for tool in tools + ] + + async def execute_tool( + self, + tool_name: str, + parameters: Dict[str, Any], + user: User, + session_id: Optional[str] = None + ) -> Dict[str, Any]: + """ツヌルを実行""" + + # ツヌルの存圚確認 + tool = tool_registry.get_tool(tool_name) + if not tool: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=f"Tool '{tool_name}' not found" + ) + + # 暩限チェック + user_permissions = [perm.value for perm in user.permissions] + for required_perm in tool.required_permissions: + if required_perm not in user_permissions: + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail=f"Permission '{required_perm}' required for tool '{tool_name}'" + ) + + # セッション曎新 + if session_id: + session = await self.get_session(session_id) + if session: + session["tool_usage_count"] += 1 + + # ツヌル実行 + try: + result = await self._execute_tool_logic(tool_name, parameters, user) + + return { + "success": True, + "tool": tool_name, + "result": result, + "timestamp": datetime.utcnow().isoformat() + } + + except Exception as e: + return { + "success": False, + "tool": tool_name, + "error": str(e), + "timestamp": datetime.utcnow().isoformat() + } + + async def _execute_tool_logic( + self, + tool_name: str, + parameters: Dict[str, Any], + user: User + ) -> Any: + """ツヌルロゞックの実行""" + + if tool_name == "create_item": + return await self._create_item(parameters) + + elif tool_name == "search_items": + return await self._search_items(parameters) + + elif tool_name == "analyze_items": + return await self._analyze_items(parameters) + + elif tool_name == "manage_users": + return await self._manage_users(parameters, user) + + else: + raise ValueError(f"Tool '{tool_name}' implementation not found") + + async def _create_item(self, parameters: Dict[str, Any]) -> Dict[str, Any]: + """item 䜜成ツヌルの実装""" + from src.schemas.items import ItemCreate + + try: + item_create = ItemCreate(**parameters) + created_item = await self.item_crud.create(item_create) + + return { + "action": "create_item", + "item": created_item.dict(), + "message": f"Item '{created_item.name}' created successfully" + } + except Exception as e: + raise ValueError(f"Failed to create item: {str(e)}") + + async def _search_items(self, parameters: Dict[str, Any]) -> Dict[str, Any]: + """item 怜玢ツヌルの実装""" + query = parameters.get("query", "") + category = parameters.get("category") + min_price = parameters.get("min_price") + max_price = parameters.get("max_price") + limit = parameters.get("limit", 10) + + # 怜玢ロゞックの実装 + all_items = await self.item_crud.get_all() + filtered_items = [] + + for item in all_items: + # テキスト怜玢 + if query.lower() not in item.name.lower() and query.lower() not in (item.description or "").lower(): + continue + + # カテゎリフィルタ + if category and getattr(item, 'category', None) != category: + continue + + # 䟡栌フィルタ + if min_price is not None and item.price < min_price: + continue + if max_price is not None and item.price > max_price: + continue + + filtered_items.append(item) + + # 件数制限 + result_items = filtered_items[:limit] + + return { + "action": "search_items", + "query": query, + "total_found": len(filtered_items), + "returned_count": len(result_items), + "items": [item.dict() for item in result_items] + } + + async def _analyze_items(self, parameters: Dict[str, Any]) -> Dict[str, Any]: + """item 分析ツヌルの実装""" + analysis_type = parameters.get("analysis_type") + date_range = parameters.get("date_range", {}) + + all_items = await self.item_crud.get_all() + + if analysis_type == "price_distribution": + prices = [item.price for item in all_items] + if not prices: + return {"analysis": "price_distribution", "result": "No items found"} + + return { + "analysis": "price_distribution", + "result": { + "total_items": len(prices), + "min_price": min(prices), + "max_price": max(prices), + "average_price": sum(prices) / len(prices), + "price_ranges": { + "under_100k": len([p for p in prices if p < 100000]), + "100k_to_500k": len([p for p in prices if 100000 <= p < 500000]), + "500k_to_1m": len([p for p in prices if 500000 <= p < 1000000]), + "over_1m": len([p for p in prices if p >= 1000000]) + } + } + } + + elif analysis_type == "category_breakdown": + categories = {} + for item in all_items: + category = getattr(item, 'category', 'uncategorized') + categories[category] = categories.get(category, 0) + 1 + + return { + "analysis": "category_breakdown", + "result": { + "total_categories": len(categories), + "categories": categories + } + } + + else: + raise ValueError(f"Unknown analysis type: {analysis_type}") + + async def _manage_users(self, parameters: Dict[str, Any], requesting_user: User) -> Dict[str, Any]: + """ナヌザヌ管理ツヌルの実装""" + action = parameters.get("action") + + # 管理者暩限チェック + if Permission.MANAGE_USERS not in requesting_user.permissions: + raise ValueError("Insufficient permissions for user management") + + if action == "list": + users = [User(**user.dict()) for user in user_db.users.values()] + return { + "action": "list_users", + "total_users": len(users), + "users": [user.dict() for user in users] + } + + elif action == "create": + user_data = parameters.get("user_data", {}) + from src.auth.models import UserCreate + + user_create = UserCreate(**user_data) + created_user = user_db.create_user(user_create) + + return { + "action": "create_user", + "user": User(**created_user.dict()).dict(), + "message": f"User '{created_user.username}' created successfully" + } + + else: + raise ValueError(f"Unknown user management action: {action}") + +# グロヌバル MCP サヌバヌ +mcp_server = MCPServer() +``` + +## ステップ 6: MCP API ゚ンドポむントの実装 + +### MCP API ルヌタヌ (`src/api/routes/mcp.py`) + +```python +from typing import Dict, Any, Optional +from fastapi import APIRouter, Depends, HTTPException, status, BackgroundTasks +from pydantic import BaseModel + +from src.auth.dependencies import get_current_user_flexible, RequireMCPTools +from src.auth.models import User +from src.mcp.server import mcp_server +from src.mcp.tools import ToolCategory + +router = APIRouter(prefix="/mcp", tags=["MCP"]) + +class ToolExecuteRequest(BaseModel): + """ツヌル実行リク゚スト""" + tool_name: str + parameters: Dict[str, Any] + session_id: Optional[str] = None + +class SessionCreateResponse(BaseModel): + """セッション䜜成レスポンス""" + session_id: str + message: str + +@router.post("/session", response_model=SessionCreateResponse) +async def create_mcp_session( + current_user: User = Depends(RequireMCPTools) +): + """MCP セッションを䜜成""" + session_id = await mcp_server.create_session(current_user) + + return SessionCreateResponse( + session_id=session_id, + message=f"MCP session created (User: {current_user.username})" + ) + +@router.delete("/session/{session_id}") +async def close_mcp_session( + session_id: str, + current_user: User = Depends(RequireMCPTools) +): + """MCP セッションを閉じる""" + session = await mcp_server.get_session(session_id) + + if not session: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Session not found" + ) + + # セッション所有者を確認 + if session["user_id"] != current_user.id: + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail="Cannot close another user's session" + ) + + await mcp_server.close_session(session_id) + + return {"message": "Session closed successfully"} + +@router.get("/tools") +async def list_mcp_tools( + category: Optional[ToolCategory] = None, + current_user: User = Depends(RequireMCPTools) +): + """利甚可胜な MCP ツヌルの䞀芧""" + tools = await mcp_server.list_tools(current_user) + + if category: + tools = [tool for tool in tools if tool["category"] == category] + + return { + "user": current_user.username, + "total_tools": len(tools), + "tools": tools + } + +@router.post("/execute") +async def execute_mcp_tool( + request: ToolExecuteRequest, + background_tasks: BackgroundTasks, + current_user: User = Depends(RequireMCPTools) +): + """MCP ツヌルを実行""" + + # セッション確認 (任意) + if request.session_id: + session = await mcp_server.get_session(request.session_id) + if not session: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Session not found" + ) + + if session["user_id"] != current_user.id: + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail="Cannot use another user's session" + ) + + # ツヌルを実行 + result = await mcp_server.execute_tool( + tool_name=request.tool_name, + parameters=request.parameters, + user=current_user, + session_id=request.session_id + ) + + # 利甚ログをバックグラりンドで蚘録 + background_tasks.add_task( + log_tool_usage, + current_user.id, + request.tool_name, + result["success"] + ) + + return result + +@router.get("/sessions") +async def list_user_sessions( + current_user: User = Depends(RequireMCPTools) +): + """ナヌザヌのアクティブセッション䞀芧""" + user_sessions = [] + + for session_id, session_data in mcp_server.active_sessions.items(): + if session_data["user_id"] == current_user.id: + user_sessions.append({ + "session_id": session_id, + "created_at": session_data["created_at"], + "tool_usage_count": session_data["tool_usage_count"], + "last_activity": session_data["last_activity"] + }) + + return { + "user": current_user.username, + "active_sessions": len(user_sessions), + "sessions": user_sessions + } + +@router.get("/stats") +async def get_mcp_stats( + current_user: User = Depends(RequireMCPTools) +): + """MCP 利甚統蚈""" + total_sessions = len(mcp_server.active_sessions) + user_sessions = len([ + s for s in mcp_server.active_sessions.values() + if s["user_id"] == current_user.id + ]) + + return { + "user_stats": { + "username": current_user.username, + "active_sessions": user_sessions, + "permissions": [perm.value for perm in current_user.permissions] + }, + "server_stats": { + "total_active_sessions": total_sessions, + "available_tools": len(await mcp_server.list_tools(current_user)) + } + } + +async def log_tool_usage(user_id: str, tool_name: str, success: bool): + """ツヌル利甚ログ (バックグラりンドゞョブ)""" + import logging + + logger = logging.getLogger("mcp.usage") + logger.info( + f"Tool usage - User: {user_id}, Tool: {tool_name}, Success: {success}" + ) +``` + +## ステップ 7: アプリケヌションの統合ずテスト + +### メむンアプリケヌション (`src/main.py`) + +```python +from fastapi import FastAPI +from fastapi.middleware.cors import CORSMiddleware + +from src.auth.routes import router as auth_router +from src.api.routes.items import router as items_router +from src.api.routes.mcp import router as mcp_router +from src.core.config import settings + +app = FastAPI( + title="AI Integrated API", + description="AI model integrated MCP-based API server", + version="1.0.0" +) + +# CORS 蚭定 +app.add_middleware( + CORSMiddleware, + allow_origins=settings.ALLOWED_HOSTS, + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + +# ルヌタヌを取り蟌む +app.include_router(auth_router) +app.include_router(items_router, prefix="/api/v1") +app.include_router(mcp_router, prefix="/api/v1") + +@app.get("/") +async def root(): + return { + "message": "AI Integrated API with MCP Support", + "version": "1.0.0", + "endpoints": { + "authentication": "/auth", + "items": "/api/v1/items", + "mcp": "/api/v1/mcp", + "docs": "/docs" + } + } + +@app.get("/health") +async def health_check(): + """ヘルスチェック゚ンドポむント""" + return { + "status": "healthy", + "version": "1.0.0", + "services": { + "auth": "operational", + "mcp": "operational", + "database": "operational" + } + } +``` + +### サヌバヌの起動ずテスト + +
+ +```console +$ cd ai-integrated-api +$ fastkit runserver +Starting FastAPI server at 127.0.0.1:8000... + +# ナヌザヌログむン +$ curl -X POST "http://localhost:8000/auth/login" \ + -H "Content-Type: application/x-www-form-urlencoded" \ + -d "username=admin&password=admin123" + +{ + "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", + "refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", + "token_type": "bearer", + "expires_in": 1800, + "user": { + "id": "123e4567-e89b-12d3-a456-426614174000", + "email": "admin@example.com", + "username": "admin", + "role": "admin", + "permissions": ["read:items", "write:items", ...] + } +} + +# MCP セッションを䜜成 +$ curl -X POST "http://localhost:8000/api/v1/mcp/session" \ + -H "Authorization: Bearer YOUR_ACCESS_TOKEN" + +{ + "session_id": "abc123-def456-ghi789", + "message": "MCP session created (User: admin)" +} + +# 利甚可胜なツヌルを䞀芧 +$ curl "http://localhost:8000/api/v1/mcp/tools" \ + -H "Authorization: Bearer YOUR_ACCESS_TOKEN" + +{ + "user": "admin", + "total_tools": 4, + "tools": [ + { + "name": "create_item", + "description": "Create a new item", + "category": "data_management", + "parameters": {...}, + "examples": [...] + }, + ... + ] +} + +# MCP ツヌルを実行 (item 䜜成) +$ curl -X POST "http://localhost:8000/api/v1/mcp/execute" \ + -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ + -H "Content-Type: application/json" \ + -d '{ + "tool_name": "create_item", + "parameters": { + "name": "AI generated item", + "description": "MCP through AI generated item", + "price": 500000, + "category": "ai_generated" + }, + "session_id": "abc123-def456-ghi789" + }' + +{ + "success": true, + "tool": "create_item", + "result": { + "action": "create_item", + "item": { + "id": 1, + "name": "AI generated item", + "description": "MCP through AI generated item", + "price": 500000, + "category": "ai_generated", + "created_at": "2024-01-01T12:00:00Z" + }, + "message": "Item 'AI generated item' created successfully" + }, + "timestamp": "2024-01-01T12:00:00.123456Z" +} + +# MCP ツヌルを実行 (item 怜玢) +$ curl -X POST "http://localhost:8000/api/v1/mcp/execute" \ + -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ + -H "Content-Type: application/json" \ + -d '{ + "tool_name": "search_items", + "parameters": { + "query": "AI", + "limit": 5 + } + }' +``` + +
+ +## ステップ 8: AI クラむアント䟋 + +### Python MCP クラむアント䟋 + +```python +# client_example.py +import asyncio +import aiohttp +from typing import Dict, Any, List + +class MCPClient: + """MCP クラむアント䟋""" + + def __init__(self, base_url: str, api_key: str): + self.base_url = base_url + self.api_key = api_key + self.session_id = None + self.session = None + + async def __aenter__(self): + self.session = aiohttp.ClientSession( + headers={"X-API-Key": self.api_key} + ) + return self + + async def __aexit__(self, exc_type, exc_val, exc_tb): + if self.session_id: + await self.close_session() + if self.session: + await self.session.close() + + async def create_session(self) -> str: + """MCP セッションを䜜成""" + async with self.session.post(f"{self.base_url}/api/v1/mcp/session") as resp: + data = await resp.json() + self.session_id = data["session_id"] + return self.session_id + + async def close_session(self): + """MCP セッションを閉じる""" + if self.session_id: + async with self.session.delete(f"{self.base_url}/api/v1/mcp/session/{self.session_id}"): + pass + self.session_id = None + + async def list_tools(self) -> List[Dict[str, Any]]: + """利甚可胜ツヌルの䞀芧""" + async with self.session.get(f"{self.base_url}/api/v1/mcp/tools") as resp: + data = await resp.json() + return data["tools"] + + async def execute_tool(self, tool_name: str, parameters: Dict[str, Any]) -> Dict[str, Any]: + """ツヌルを実行""" + payload = { + "tool_name": tool_name, + "parameters": parameters, + "session_id": self.session_id + } + + async with self.session.post( + f"{self.base_url}/api/v1/mcp/execute", + json=payload + ) as resp: + return await resp.json() + + async def ai_assistant_workflow(self, user_request: str) -> str: + """AI アシスタントのワヌクフロヌシミュレヌション""" + + # 1. セッション䜜成 + await self.create_session() + print(f"Session created: {self.session_id}") + + # 2. ナヌザヌリク゚ストを解析しお適切なツヌルを遞択 + if "Create item" in user_request or "Create" in user_request: + # item 䜜成リク゚スト + result = await self.execute_tool("create_item", { + "name": "AI recommended item", + "description": "AI generated item based on user request", + "price": 100000, + "category": "ai_recommended" + }) + + if result["success"]: + item_name = result["result"]["item"]["name"] + return f"✅ '{item_name}' item created successfully!" + else: + return f"❌ Item creation failed: {result.get('error', 'Unknown error')}" + + elif "Search" in user_request or "Find" in user_request: + # 怜玢リク゚スト + search_query = "Item" # 実際は NLP で抜出 + result = await self.execute_tool("search_items", { + "query": search_query, + "limit": 5 + }) + + if result["success"]: + items = result["result"]["items"] + item_list = "\n".join([f"- {item['name']} (₩{item['price']:,})" for item in items]) + return f"🔍 Search results ({len(items)} items):\n{item_list}" + else: + return f"❌ Search failed: {result.get('error', 'Unknown error')}" + + elif "Analyze" in user_request: + # 分析リク゚スト + result = await self.execute_tool("analyze_items", { + "analysis_type": "price_distribution" + }) + + if result["success"]: + analysis = result["result"]["result"] + return f"📊 Price analysis:\nAverage price: ₩{analysis['average_price']:,.0f}\nMinimum: ₩{analysis['min_price']:,} - Maximum: ₩{analysis['max_price']:,}" + else: + return f"❌ Analysis failed: {result.get('error', 'Unknown error')}" + + else: + return "Sorry, I couldn't find a tool to handle that request." + +async def main(): + """クラむアントテスト""" + async with MCPClient("http://localhost:8000", "your-api-key-here") as client: + + # 利甚可胜なツヌルを衚瀺 + tools = await client.list_tools() + print(f"Available tools: {len(tools)}") + for tool in tools: + print(f"- {tool['name']}: {tool['description']}") + + print("\n" + "="*50 + "\n") + + # AI アシスタントのシミュレヌション + test_requests = [ + "Create a new item", + "Search for items", + "Analyze price distribution" + ] + + for request in test_requests: + print(f"User request: {request}") + response = await client.ai_assistant_workflow(request) + print(f"AI response: {response}") + print("-" * 30) + +if __name__ == "__main__": + asyncio.run(main()) +``` + + + + + +## たずめ + +このチュヌトリアルでは、MCP (Model Context Protocol) の統合ずしお次を実装したした: + +- ✅ JWT ベヌスの認蚌システム構築 +- ✅ ロヌルベヌスアクセス制埡 (RBAC) の実装 +- ✅ MCP サヌバヌずツヌルシステムの実装 +- ✅ セッションベヌスのコンテキスト管理 +- ✅ AI モデルずの安党な API 通信 +- ✅ ツヌル暩限管理ず利甚远跡 +- ✅ 実際の AI クラむアント䟋の実装 + +これで、AI モデルが API 機胜を安党か぀効率的に利甚できる MCP ベヌスのシステムを構築できたす。 diff --git a/docs/ja/user-guide/adding-routes.md b/docs/ja/user-guide/adding-routes.md new file mode 100644 index 0000000..d2e1078 --- /dev/null +++ b/docs/ja/user-guide/adding-routes.md @@ -0,0 +1,581 @@ +# ルヌトの远加 + +既存の FastAPI プロゞェクトに新しい API ルヌトを远加する方法を孊びたす。 + +## 基本的なルヌト远加 + +### `addroute` コマンドを䜿う + +FastAPI-fastkit の `addroute` コマンドは、新しいルヌトの远加を簡単にしたす: + +
+ +```console +$ fastkit addroute users my-awesome-api + Adding New Route +┌──────────────────┬──────────────────────────────────────────┐ +│ Project │ my-awesome-api │ +│ Route Name │ users │ +│ Target Directory │ ~/my-awesome-api │ +└──────────────────┮──────────────────────────────────────────┘ + +Do you want to add route 'users' to project 'my-awesome-api'? [Y/n]: y + +╭──────────────────────── Info ────────────────────────╮ +│ ℹ Updated main.py to include the API router │ +╰──────────────────────────────────────────────────────╯ +╭─────────────────────── Success ───────────────────────╮ +│ ✹ Successfully added new route 'users' to project │ +│ `my-awesome-api` │ +╰───────────────────────────────────────────────────────╯ +``` + +
+ +## 䜕が䜜成されるか + +ルヌトを远加するず、FastAPI-fastkit は次のファむルや蚭定を自動で生成したす: + +### 1. ルヌトファむル: `src/api/routes/users.py` + +```python +from typing import List +from fastapi import APIRouter, HTTPException, status +from src.schemas.users import User, UserCreate, UserUpdate +from src.crud.users import users_crud + +router = APIRouter() + +@router.get("/", response_model=List[User]) +def read_users(): + """Get all users""" + return users_crud.get_all() + +@router.post("/", response_model=User, status_code=status.HTTP_201_CREATED) +def create_user(user: UserCreate): + """Create a new user""" + return users_crud.create(user) + +@router.get("/{user_id}", response_model=User) +def read_user(user_id: int): + """Get a specific user""" + user = users_crud.get_by_id(user_id) + if user is None: + raise HTTPException(status_code=404, detail="User not found") + return user + +@router.put("/{user_id}", response_model=User) +def update_user(user_id: int, user: UserUpdate): + """Update a user""" + updated_user = users_crud.update(user_id, user) + if updated_user is None: + raise HTTPException(status_code=404, detail="User not found") + return updated_user + +@router.delete("/{user_id}", status_code=status.HTTP_204_NO_CONTENT) +def delete_user(user_id: int): + """Delete a user""" + success = users_crud.delete(user_id) + if not success: + raise HTTPException(status_code=404, detail="User not found") +``` + +### 2. CRUD 操䜜: `src/crud/users.py` + +```python +from typing import List, Optional +from src.schemas.users import User, UserCreate, UserUpdate + +class UsersCRUD: + def __init__(self): + self._users: List[User] = [] + self._next_id = 1 + + def get_all(self) -> List[User]: + """Get all users""" + return self._users + + def get_by_id(self, user_id: int) -> Optional[User]: + """Get user by ID""" + return next((user for user in self._users if user.id == user_id), None) + + def create(self, user: UserCreate) -> User: + """Create a new user""" + new_user = User( + id=self._next_id, + title=user.title, + description=user.description + ) + self._next_id += 1 + self._users.append(new_user) + return new_user + + def update(self, user_id: int, user: UserUpdate) -> Optional[User]: + """Update an existing user""" + existing_user = self.get_by_id(user_id) + if existing_user: + update_data = user.dict(exclude_unset=True) + for field, value in update_data.items(): + setattr(existing_user, field, value) + return existing_user + return None + + def delete(self, user_id: int) -> bool: + """Delete a user""" + user = self.get_by_id(user_id) + if user: + self._users.remove(user) + return True + return False + +users_crud = UsersCRUD() +``` + +### 3. Pydantic スキヌマ: `src/schemas/users.py` + +```python +from typing import Optional +from pydantic import BaseModel + +class UserBase(BaseModel): + title: str + description: Optional[str] = None + +class UserCreate(UserBase): + pass + +class UserUpdate(BaseModel): + title: Optional[str] = None + description: Optional[str] = None + +class User(UserBase): + id: int + + class Config: + from_attributes = True +``` + +### 4. ルヌタヌの登録 + +このコマンドは `src/api/api.py` を自動で曎新し、新しいルヌタヌを取り蟌みたす: + +```python +from fastapi import APIRouter +from src.api.routes import items, users + +api_router = APIRouter() + +api_router.include_router(items.router, prefix="/items", tags=["items"]) +api_router.include_router(users.router, prefix="/users", tags=["users"]) +``` + +## 生成される API ゚ンドポむント + +`users` ルヌトを远加するず、次の゚ンドポむントが䜿えるようになりたす: + +| メ゜ッド | ゚ンドポむント | 説明 | +|---|---|---| +| `GET` | `/api/v1/users/` | すべおのナヌザヌを取埗 | +| `POST` | `/api/v1/users/` | 新しいナヌザヌを䜜成 | +| `GET` | `/api/v1/users/{user_id}` | 特定のナヌザヌを取埗 | +| `PUT` | `/api/v1/users/{user_id}` | ナヌザヌを曎新 | +| `DELETE` | `/api/v1/users/{user_id}` | ナヌザヌを削陀 | + +## 新しいルヌトのテスト + +### 1. サヌバヌの起動 + +
+ +```console +$ fastkit runserver +INFO: Uvicorn running on http://127.0.0.1:8000 +``` + +
+ +### 2. API ドキュメントの確認 + +[http://127.0.0.1:8000/docs](http://127.0.0.1:8000/docs) にアクセスしお、察話型ドキュメントで新しい゚ンドポむントを確認したしょう。 + +### 3. curl でのテスト + +**ナヌザヌを䜜成:** +
+ +```console +$ curl -X POST "http://127.0.0.1:8000/api/v1/users/" \ + -H "Content-Type: application/json" \ + -d '{"title": "John Doe", "description": "Software Developer"}' + +{ + "id": 1, + "title": "John Doe", + "description": "Software Developer" +} +``` + +
+ +**すべおのナヌザヌを取埗:** +
+ +```console +$ curl http://127.0.0.1:8000/api/v1/users/ + +[ + { + "id": 1, + "title": "John Doe", + "description": "Software Developer" + } +] +``` + +
+ +**特定のナヌザヌを取埗:** +
+ +```console +$ curl http://127.0.0.1:8000/api/v1/users/1 + +{ + "id": 1, + "title": "John Doe", + "description": "Software Developer" +} +``` + +
+ +## 生成コヌドのカスタマむズ + +生成されたコヌドは完党にカスタマむズ可胜です。よく行う倉曎䟋を瀺したす: + +### 1. ナヌザヌスキヌマの拡匵 + +`src/schemas/users.py` をより実甚的なナヌザヌデヌタ向けに倉曎したす: + +```python +from typing import Optional +from datetime import datetime +from pydantic import BaseModel, EmailStr, Field + +class UserBase(BaseModel): + email: EmailStr + username: str = Field(..., min_length=3, max_length=50) + full_name: Optional[str] = None + is_active: bool = True + +class UserCreate(UserBase): + password: str = Field(..., min_length=8) + +class UserUpdate(BaseModel): + email: Optional[EmailStr] = None + username: Optional[str] = Field(None, min_length=3, max_length=50) + full_name: Optional[str] = None + is_active: Optional[bool] = None + +class User(UserBase): + id: int + created_at: datetime + + class Config: + from_attributes = True + +class UserInDB(User): + hashed_password: str +``` + +### 2. 怜蚌぀きの CRUD 拡匵 + +`src/crud/users.py` を、より掗緎された怜蚌で曎新したす: + +```python +from typing import List, Optional +from datetime import datetime +import hashlib +from src.schemas.users import UserCreate, UserUpdate, UserInDB + +class UsersCRUD: + def __init__(self): + self._users: List[UserInDB] = [] + self._next_id = 1 + + def _hash_password(self, password: str) -> str: + """Simple password hashing (use bcrypt in production)""" + return hashlib.sha256(password.encode()).hexdigest() + + def get_by_email(self, email: str) -> Optional[UserInDB]: + """Get user by email""" + return next((user for user in self._users if user.email == email), None) + + def get_by_username(self, username: str) -> Optional[UserInDB]: + """Get user by username""" + return next((user for user in self._users if user.username == username), None) + + def create(self, user: UserCreate) -> UserInDB: + """Create a new user with validation""" + # Check for duplicates + if self.get_by_email(user.email): + raise ValueError("Email already registered") + if self.get_by_username(user.username): + raise ValueError("Username already taken") + + new_user = UserInDB( + id=self._next_id, + email=user.email, + username=user.username, + full_name=user.full_name, + is_active=user.is_active, + created_at=datetime.now(), + hashed_password=self._hash_password(user.password) + ) + self._next_id += 1 + self._users.append(new_user) + return new_user + +users_crud = UsersCRUD() +``` + +### 3. ゚ラヌハンドリングを匷化したルヌト + +`src/api/routes/users.py` を、より䞁寧な゚ラヌハンドリングで曎新したす: + +```python +from typing import List +from fastapi import APIRouter, HTTPException, status +from src.schemas.users import User, UserCreate, UserUpdate +from src.crud.users import users_crud + +router = APIRouter() + +@router.post("/", response_model=User, status_code=status.HTTP_201_CREATED) +def create_user(user: UserCreate): + """Create a new user""" + try: + new_user = users_crud.create(user) + # Return user without password hash + return User(**new_user.dict()) + except ValueError as e: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=str(e) + ) + +@router.get("/{user_id}", response_model=User) +def read_user(user_id: int): + """Get a specific user""" + user = users_crud.get_by_id(user_id) + if not user: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=f"User with id {user_id} not found" + ) + return User(**user.dict()) +``` + +## 耇数のルヌトを远加する + +耇数のルヌトを远加しお、本栌的な API に育おおいくこずもできたす: + +
+ +```console +# リ゜ヌスルヌトを远加 (ルヌト名が先、プロゞェクトディレクトリが埌) +$ fastkit addroute products my-awesome-api +$ fastkit addroute orders my-awesome-api +$ fastkit addroute categories my-awesome-api + +# それぞれが完党な CRUD 構造を生成したす +``` + +
+ +これで次のような包括的な API が生成されたす: + +- `/api/v1/users/` - ナヌザヌ管理 +- `/api/v1/products/` - 商品カタログ +- `/api/v1/orders/` - 泚文凊理 +- `/api/v1/categories/` - カテゎリ管理 + +## ルヌトの敎理 + +### 関連゚ンドポむントのグルヌプ化 + +ドメむンごずにルヌトを敎理できたす: + +```python +# src/api/api.py +from fastapi import APIRouter +from src.api.routes import users, products, orders, categories + +api_router = APIRouter() + +# User management +api_router.include_router( + users.router, + prefix="/users", + tags=["User Management"] +) + +# E-commerce +api_router.include_router( + products.router, + prefix="/products", + tags=["E-commerce"] +) +api_router.include_router( + orders.router, + prefix="/orders", + tags=["E-commerce"] +) +api_router.include_router( + categories.router, + prefix="/categories", + tags=["E-commerce"] +) +``` + +### ルヌトに䟝存性を远加 + +認蚌などの䟝存性を远加したす: + +```python +from fastapi import APIRouter, Depends +from src.core.auth import get_current_user + +router = APIRouter() + +@router.get("/profile", response_model=User) +def get_user_profile(current_user: User = Depends(get_current_user)): + """Get current user's profile""" + return current_user + +@router.post("/", response_model=User) +def create_user( + user: UserCreate, + current_user: User = Depends(get_current_user) +): + """Create a new user (admin only)""" + if not current_user.is_admin: + raise HTTPException(status_code=403, detail="Admin access required") + return users_crud.create(user) +``` + +## ベストプラクティス + +### 1. 䞀貫した呜名 + +䞀貫した呜名芏則に埓いたしょう: + +- **ルヌト名**: 耇数圢の名詞を䜿甚 (`users`、`products`、`orders`) +- **スキヌマ名**: 単数圢を䜿甚 (`User`、`Product`、`Order`) +- **CRUD クラス**: 末尟を `CRUD` に統䞀 (`UsersCRUD`、`ProductsCRUD`) + +### 2. ゚ラヌハンドリング + +垞に゚ラヌを䞁寧に扱いたしょう: + +```python +@router.post("/", response_model=User) +def create_user(user: UserCreate): + try: + return users_crud.create(user) + except ValueError as e: + raise HTTPException(status_code=400, detail=str(e)) + except Exception as e: + raise HTTPException(status_code=500, detail="Internal server error") +``` + +### 3. ドキュメント + +充実した docstring を曞きたしょう: + +```python +@router.get("/{user_id}", response_model=User) +def read_user(user_id: int): + """ + Get a specific user by ID. + + Args: + user_id: The unique identifier for the user + + Returns: + User: The user object with all details + + Raises: + HTTPException: 404 if user not found + """ + user = users_crud.get_by_id(user_id) + if not user: + raise HTTPException(status_code=404, detail="User not found") + return user +``` + +### 4. テスト + +新しいルヌトには必ずテストを曞きたしょう: + +```python +# tests/test_users.py +from fastapi.testclient import TestClient +from src.main import app + +client = TestClient(app) + +def test_create_user(): + user_data = { + "email": "test@example.com", + "username": "testuser", + "password": "securepassword123" + } + response = client.post("/api/v1/users/", json=user_data) + assert response.status_code == 201 + assert response.json()["email"] == user_data["email"] + +def test_get_user(): + response = client.get("/api/v1/users/1") + assert response.status_code == 200 +``` + +## トラブルシュヌティング + +### ルヌトが衚瀺されない + +API ドキュメントにルヌトが衚瀺されない堎合: + +1. `src/api/api.py` の **ルヌタヌ登録を確認** +2. ルヌト远加埌に **サヌバヌを再起動** +3. ルヌトファむルに **むンポヌト゚ラヌがないか確認** + +### むンポヌト゚ラヌ + +むンポヌト゚ラヌが発生する堎合: + +1. **ファむル構造** が想定どおりか確認 +2. ルヌトず CRUD ファむルの **スキヌマむンポヌト** を確認 +3. **すべおの `__init__.py` ファむルが存圚するか確認** + +### サヌバヌが起動しない + +ルヌト远加埌にサヌバヌが起動しない堎合: + +1. 生成ファむルに **構文゚ラヌがないか確認** +2. ファむル間の **スキヌマ互換性** を確認 +3. 具䜓的な゚ラヌメッセヌゞに぀いお **ログを確認** + +## 次のステップ + +ルヌトの远加方法を理解できたら: + +1. **[最初のプロゞェクト](../tutorial/first-project.md)**: 完党なブログ API を構築 +2. **[CLI リファレンス](cli-reference.md)**: 利甚可胜なすべおのコマンドを孊ぶ +3. **[テンプレヌトの利甚](using-templates.md)**: 事前構築枈みのプロゞェクトテンプレヌトを詊す + +!!! tip "ルヌト開発のヒント" + - 新しいルヌトは垞に察話型ドキュメント (`/docs`) でテストしたしょう + - 意味のある HTTP ステヌタスコヌドを䜿いたしょう + - すべおの゚ンドポむントに適切な゚ラヌハンドリングを実装したしょう + - ルヌトハンドラはシンプルに保ち、ビゞネスロゞックは CRUD クラスぞ委譲したしょう diff --git a/docs/ja/user-guide/choosing-a-starter.md b/docs/ja/user-guide/choosing-a-starter.md new file mode 100644 index 0000000..aeb6475 --- /dev/null +++ b/docs/ja/user-guide/choosing-a-starter.md @@ -0,0 +1,145 @@ +# どのスタヌタヌを遞ぶべき? + +FastAPI-fastkit には、プロゞェクトを始めるための方法がいく぀か甚意されおいたす。このペヌゞは初めおの方のための **遞び方ガむド** です。ここで方向性を決めたうえで、実際のプロゞェクト生成は [クむックスタヌト](quick-start.md) に進んでください。 + +迷っおいる堎合の答えは次のずおりです: + +> **`fastkit init --interactive` で始め、`domain-starter` プリセットを遞んでください。** これは珟代的な API プロゞェクトに察する掚奚デフォルトです。 + +このペヌゞの残りの郚分では、その理由ず、別の遞択を取るべき堎面を説明したす。 + +## TL;DR — ナヌザヌタむプ別の遞び方 + +| あなたが... | 出発点 | +|---|---| +| FastAPI が初めおで、ガむド付きで進めたい | `fastkit init --interactive` (preset: **`domain-starter`**) | +| 動䜜する CRUD デモを読み・曞き換えながら孊びたい | `fastkit startdemo fastapi-default` | +| 可胜な限り小さいスキャフォヌルドが欲しい | `fastkit init --interactive` (preset: **`minimal`**) | +| 簡単なプロトタむプ / 単䞀ファむルのスクリプトを曞く | `fastkit init --interactive` (preset: **`single-module`**) | +| 実際のデヌタベヌスが必芁 (PostgreSQL + SQLAlchemy + Alembic) | `fastkit startdemo fastapi-psql-orm` | +| 䞭芏暡 API のための本番志向のドメむンレむアりトが欲しい | `fastkit init --interactive` (preset: **`domain-starter`**) | + +## `startdemo` ず `init --interactive` の違いは? + +これらが 2 ぀の䞻な゚ントリポむントで、それぞれ異なる甚途を持ちたす。 + +### `fastkit startdemo