diff --git a/docs/en/reference/translation-status.md b/docs/en/reference/translation-status.md index b5fa610..59e4750 100644 --- a/docs/en/reference/translation-status.md +++ b/docs/en/reference/translation-status.md @@ -38,10 +38,10 @@ next section explains). | 🇯🇵 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`) | ✅ 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/es/changelog.md` intentionally reuses the canonical English `CHANGELOG.md`. | -| 🇫🇷 French (`fr`) | 🔴 Skeleton | 0 / 26 | Build target only. Every page falls back to English. | +| 🇫🇷 French (`fr`) | ✅ 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/fr/changelog.md` intentionally reuses the canonical English `CHANGELOG.md`. | | 🇩🇪 German (`de`) | 🔴 Skeleton | 0 / 26 | Build target only. Every page falls back to English. | -*Snapshot verified 2026-05-11; es row recounted for the current branch after Phase 3 (contributing + reference) landed. Spanish now has all locale pages present, while `docs/es/changelog.md` intentionally points to the canonical English changelog.* These counts are maintained by hand; +*Snapshot verified 2026-05-15; fr row recounted for the current branch after Phase 3 (contributing + reference) landed. French now has all locale pages present, while `docs/fr/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/es/reference/translation-status.md b/docs/es/reference/translation-status.md index e2494ab..63b29d1 100644 --- a/docs/es/reference/translation-status.md +++ b/docs/es/reference/translation-status.md @@ -23,10 +23,10 @@ Los números siguientes cuentan páginas Markdown presentes en el árbol de cada | 🇯🇵 Japonés (`ja`) | ✅ Completo | 26 / 26 | Todas las páginas del idioma están presentes. Phase 1: nivel superior + user-guide principal; Phase 2: user-guide restante + todos los tutorials; Phase 3: contributing + reference. `docs/ja/changelog.md` reutiliza intencionadamente el `CHANGELOG.md` canónico en inglés. | | 🇪🇸 Español (`es`) | ✅ Completo | 26 / 26 | Todas las páginas del idioma están presentes. Phase 1: nivel superior + user-guide principal; Phase 2: user-guide restante + todos los tutorials; Phase 3: contributing + reference. `docs/es/changelog.md` reutiliza intencionadamente el `CHANGELOG.md` canónico en inglés. | | 🇨🇳 Chino (`zh`) | 🔴 Esqueleto | 0 / 26 | Solo está configurado como destino de compilación. Cada página muestra la versión en inglés. | -| 🇫🇷 Francés (`fr`) | 🔴 Esqueleto | 0 / 26 | Solo está configurado como destino de compilación. Cada página muestra la versión en inglés. | +| 🇫🇷 Francés (`fr`) | ✅ Completo | 26 / 26 | Todas las páginas del idioma están presentes. Phase 1: nivel superior + núcleo del user-guide; Phase 2: resto del user-guide + todos los tutoriales; Phase 3: contributing + reference. `docs/fr/changelog.md` reutiliza intencionadamente el `CHANGELOG.md` canónico en inglés. | | 🇩🇪 Alemán (`de`) | 🔴 Esqueleto | 0 / 26 | Solo está configurado como destino de compilación. Cada página muestra la versión en inglés. | -*Verificado el 2026-05-11; la fila de `es` se volvió a contar para la rama actual tras completar Phase 3 (contributing + reference). El español ya tiene todas las páginas del idioma presentes, mientras que `docs/es/changelog.md` apunta al changelog canónico en inglés.* Esta cuenta se mantiene a mano; para volver a contar el estado actual desde la raíz del repositorio, ejecuta: +*Verificado el 2026-05-15; la fila de `fr` se volvió a contar para la rama actual tras completar Phase 3 (contributing + reference). El francés ya tiene todas las páginas del idioma presentes, mientras que `docs/fr/changelog.md` apunta al changelog canónico en inglés.* Esta cuenta se mantiene a mano; para volver a contar el estado actual desde la raíz del repositorio, ejecuta: ```console $ for loc in en ko ja zh es fr de; do diff --git a/docs/fr/changelog.md b/docs/fr/changelog.md new file mode 100644 index 0000000..bf802e6 --- /dev/null +++ b/docs/fr/changelog.md @@ -0,0 +1,4 @@ +> Cette page réutilise le `CHANGELOG.md` canonique en anglais à la racine du dépôt. +> L'historique des versions n'est pas maintenu séparément en français. + +{!CHANGELOG.md!} diff --git a/docs/fr/contributing/code-guidelines.md b/docs/fr/contributing/code-guidelines.md new file mode 100644 index 0000000..155eeb4 --- /dev/null +++ b/docs/fr/contributing/code-guidelines.md @@ -0,0 +1,748 @@ +# Directives de code + +Standards de codage et bonnes pratiques complets pour contribuer à FastAPI-fastkit. + +## Vue d'ensemble + +Ces directives garantissent la qualité, la cohérence et la maintenabilité du code dans l'ensemble du projet FastAPI-fastkit. Suivre ces standards aide à créer une base de code facile à lire, à maintenir et à étendre. + +## Style de code Python + +### Conformité PEP 8 + +Suivez [PEP 8](https://www.python.org/dev/peps/pep-0008/) avec ces configurations spécifiques : + +- **Longueur de ligne** : 88 caractères (par défaut de Black) +- **Indentation** : 4 espaces (pas de tabulations) +- **Virgules finales** : requises dans les structures multi-lignes +- **Guillemets** : guillemets doubles préférés + +### Mise en forme du code + +Nous utilisons **Black** pour la mise en forme automatique du code : + +```python +# Bon ✅ +def create_project( + name: str, + template: str, + options: Dict[str, Any], +) -> ProjectResult: + """Create a new FastAPI project.""" + return ProjectResult(name=name, template=template) + +# À éviter ❌ +def create_project(name: str, template: str, options: Dict[str,Any])->ProjectResult: + """Create a new FastAPI project.""" + return ProjectResult(name=name,template=template) +``` + +### Organisation des imports + +Utilisez **isort** pour organiser les imports : + +```python +# Standard library imports +import os +import sys +from pathlib import Path +from typing import Dict, List, Optional, Union + +# Third-party imports +import click +import pydantic +from fastapi import FastAPI + +# Local imports +from fastapi_fastkit.commands import BaseCommand +from fastapi_fastkit.utils import validation +from fastapi_fastkit.templates.manager import TemplateManager +``` + +## Annotations de type + +### Annotations de type requises + +Toutes les fonctions et méthodes publiques doivent inclure des annotations de type : + +```python +# Bon ✅ +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: + # Implementation... + created_files.append(file_path) + return created_files + +# À éviter ❌ +def validate_project_name(name): + return name.isidentifier() and not name.startswith('_') +``` + +### Annotations de type complexes + +Utilisez les annotations de type appropriées pour les structures complexes : + +```python +from typing import Dict, List, Optional, Union, Tuple, Any +from pathlib import Path + +# Type aliases for complex types +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.""" + # Implementation... + return True, None +``` + +## Conventions de nommage + +### Variables et fonctions + +- **snake_case** pour les variables et les fonctions +- **Noms explicites** qui décrivent l'usage +- **Évitez les abréviations** sauf si elles sont largement comprises + +```python +# Bon ✅ +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 + +# À éviter ❌ +proj_nm = "my-api" +temp_dir = Path("templates") +usr_data = get_input() + +def validate_email(e): + return "@" in e and "." in e +``` + +### Classes + +- **PascalCase** pour les noms de classes +- Noms **explicites et précis** + +```python +# Bon ✅ +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 + +# À éviter ❌ +class Class: + pass + +class Error(Exception): + pass + +class Handler: + pass +``` + +### Constantes + +- **UPPER_CASE** avec des underscores +- Constantes **au niveau du module** uniquement + +```python +# Bon ✅ +DEFAULT_TEMPLATE_NAME = "fastapi-default" +MAX_PROJECT_NAME_LENGTH = 50 +SUPPORTED_PYTHON_VERSIONS = ["3.8", "3.9", "3.10", "3.11", "3.12"] + +# À éviter ❌ +default_template = "fastapi-default" +maxLength = 50 +versions = ["3.8", "3.9", "3.10", "3.11", "3.12"] +``` + +## Standards de documentation + +### Docstrings + +Utilisez les **docstrings de style Google** pour toutes les API publiques : + +```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") + ``` + """ + # Implementation here... + pass +``` + +### Commentaires + +- **Expliquez le POURQUOI, pas le QUOI** +- **Utilisez-les avec parcimonie** — le code doit s'auto-documenter +- **Mettez à jour les commentaires** quand le code change + +```python +# Bon ✅ +def validate_dependencies(requirements: List[str]) -> bool: + """Validate project dependencies.""" + # Skip validation in development mode to allow experimental packages + if os.getenv("FASTKIT_DEV_MODE"): + return True + + # Check each requirement against known security vulnerabilities + for requirement in requirements: + if is_vulnerable_package(requirement): + return False + + return True + +# À éviter ❌ +def validate_dependencies(requirements: List[str]) -> bool: + """Validate project dependencies.""" + # Check if dev mode + if os.getenv("FASTKIT_DEV_MODE"): + return True + + # Loop through requirements + for requirement in requirements: + # Check if vulnerable + if is_vulnerable_package(requirement): + return False + + # Return true + return True +``` + +## Gestion des erreurs + +### Gestion des exceptions + +- **Attrapez des exceptions précises** autant que possible +- **Fournissez des messages d'erreur explicites** +- **Journalisez les erreurs de manière appropriée** + +```python +# Bon ✅ +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}" + ) + +# À éviter ❌ +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}") +``` + +### Exceptions personnalisées + +Définissez des exceptions précises pour différentes conditions d'erreur : + +```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 +``` + +## Standards de tests + +### Structure des tests + +Organisez les tests avec une structure et un nommage clairs : + +```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() +``` + +### Couverture des tests + +- **Visez 90 %+ de couverture** sur le code nouveau +- **Testez les cas limites** et les conditions d'erreur +- **Simulez les dépendances externes** + +```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") +``` + +## Directives sur les imports + +### Organisation des imports + +!!! note + + Le formateur `isort` organise automatiquement les imports : vous pouvez donc les organiser facilement en lançant `bash scripts/format.sh`. + +1. Imports de la **bibliothèque standard** en premier +2. Imports **tiers** en deuxième +3. Imports de l'**application du projet** en dernier +4. **Ligne vide** entre chaque groupe + +```python +# Bibliothèque standard +import os +import sys +from pathlib import Path +from typing import Dict, List, Optional + +# Bibliothèques tierces +import click +import pydantic +import yaml +from fastapi import FastAPI + +# Application du projet +from fastapi_fastkit.commands.base import BaseCommand +from fastapi_fastkit.utils.validation import validate_project_name +from fastapi_fastkit.templates import TemplateManager +``` + +### Bonnes pratiques d'imports + +- **Évitez les imports avec joker** (`from module import *`) +- **Utilisez les imports absolus** pour plus de clarté +- **Importez les modules, pas des éléments précis** quand vous importez beaucoup d'éléments + +```python +# Bon ✅ +from fastapi_fastkit.utils import validation, files, formatting + +# Bon ✅ (quand vous importez peu d'éléments) +from fastapi_fastkit.utils.validation import validate_email, validate_project_name + +# À éviter ❌ +from fastapi_fastkit.utils.validation import * + +# À éviter ❌ (quand vous importez beaucoup d'éléments) +from fastapi_fastkit.utils.validation import ( + validate_email, validate_project_name, validate_template_name, + validate_dependencies, validate_python_version, validate_directory +) +``` + +## Directives de sécurité + +### Validation des entrées + +Validez et assainissez toujours les entrées utilisateur : + +```python +def validate_project_name(name: str) -> str: + """Valider et assainir le nom du projet.""" + 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)") + + # Assainir en supprimant les caractères dangereux + sanitized = re.sub(r'[^a-zA-Z0-9_-]', '', name) + + return sanitized +``` + +### Opérations sur les fichiers + +Soyez prudent avec les chemins et les opérations sur les fichiers : + +```python +def create_file_safely(file_path: Path, content: str, base_dir: Path) -> None: + """Créer un fichier de manière sûre dans le répertoire de base.""" + # Résoudre le chemin pour éviter les attaques de traversée de répertoires + resolved_path = file_path.resolve() + resolved_base = base_dir.resolve() + + # Vérifier que le fichier reste dans le répertoire de base + try: + resolved_path.relative_to(resolved_base) + except ValueError: + raise SecurityError(f"File path outside base directory: {file_path}") + + # Créer les répertoires parents en toute sécurité + resolved_path.parent.mkdir(parents=True, exist_ok=True) + + # Écrire le fichier avec des permissions appropriées + resolved_path.write_text(content, encoding='utf-8') + resolved_path.chmod(0o644) # Lecture/écriture pour le propriétaire, lecture pour les autres +``` + +## Directives de performance + +### Pratiques de code efficaces + +- **Utilisez des générateurs** pour les grands jeux de données +- **Évitez l'optimisation prématurée** +- **Profilez avant d'optimiser** + +```python +# Bon ✅ - Générateur plus économe en mémoire +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) + +# À éviter ❌ - Charge tout en mémoire +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 +``` + +### Mise en cache + +Utilisez la mise en cache pour les opérations coûteuses : + +```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) +``` + +## Directives sur les commits Git + +### Format de message de commit + +Utilisez le format de commit conventionnel : + +``` +type(scope): description + +[optional body] + +[optional footer] +``` + +### Types de commits + +- **feat** : nouvelle fonctionnalité +- **fix** : correction de bogue +- **docs** : changements de documentation +- **style** : changements de style de code (mise en forme, etc.) +- **refactor** : refactoring de code +- **test** : ajout ou mise à jour de tests +- **chore** : tâches de maintenance + +### Exemples + +```bash +# Bon ✅ +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 + +# Bon ✅ +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. + +# À éviter ❌ +update stuff + +# À éviter ❌ +Fixed bug +``` + +## Directives de relecture de code + +### Pour les auteurs + +Avant de soumettre du code à la relecture : + +1. **Lancez tous les tests** et assurez-vous qu'ils passent +2. **Vérifiez que la couverture de code** est maintenue +3. **Mettez à jour la documentation** si nécessaire +4. **Suivez les conventions** de messages de commit +5. **Gardez les pull requests** ciblées et petites + +### Pour les relecteurs + +Lorsque vous relisez du code : + +1. **Vérifiez le fonctionnement** — est-ce que ça marche comme prévu ? +2. **Relisez les tests** — les cas limites sont-ils couverts ? +3. **Vérifiez la documentation** — est-elle claire et à jour ? +4. **Vérifiez le style de code** — respecte-t-il les conventions du projet ? +5. **Pensez à la sécurité** — vulnérabilités potentielles ? + +### Liste de vérification de relecture + +- [ ] Le code respecte les directives de style +- [ ] Les tests sont complets et passent +- [ ] La documentation est à jour +- [ ] Aucune vulnérabilité de sécurité +- [ ] Les considérations de performance sont prises en compte +- [ ] La gestion des erreurs est appropriée +- [ ] Les messages de commit respectent les conventions + +## Outils et automatisation + +### Hooks pre-commit + +Nous utilisons les hooks pre-commit pour faire respecter les standards : + +```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 + + Les hooks pre-commit utilisent des environnements Python isolés (`language: python`). + +### Configuration de l'IDE + +Paramètres VS Code recommandés : + +```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 + } +} +``` + +## Étapes suivantes + +Après avoir parcouru ces directives : + +1. **Mettez en place l'environnement de développement** en suivant [Configuration du développement](development-setup.md) +2. **Pratiquez avec de petites contributions** pour vous familiariser +3. **Posez des questions** dans GitHub Discussions si quelque chose n'est pas clair +4. **Relisez le code existant** pour voir ces directives en pratique + +!!! tip "Référence rapide" + - Utilisez `make check-all` pour vérifier que votre code respecte toutes les directives + - Configurez les hooks pre-commit pour détecter les problèmes tôt + - En cas de doute, regardez le code existant pour des exemples + - N'hésitez pas à demander de l'aide lors des relectures de code + +Suivre ces directives aide à maintenir la haute qualité de code de FastAPI-fastkit et facilite la collaboration pour tous ! 🚀 diff --git a/docs/fr/contributing/development-setup.md b/docs/fr/contributing/development-setup.md new file mode 100644 index 0000000..52f92b8 --- /dev/null +++ b/docs/fr/contributing/development-setup.md @@ -0,0 +1,816 @@ +# Configuration du développement + +Un guide complet pour mettre en place un environnement de développement afin de contribuer à FastAPI-fastkit. + +## Prérequis + +Avant de commencer, assurez-vous d'avoir : + +- **Python 3.12 ou supérieur** installé +- **Git** installé et configuré +- **Connaissances de base** de Python et de FastAPI +- **Un éditeur de texte ou IDE** (VS Code, PyCharm, etc.) + +## Configuration rapide avec le Makefile + +FastAPI-fastkit fournit un Makefile pour une configuration facile du développement : + +
+ +```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! +``` + +
+ +Cette commande unique : + +- Installe le paquet en mode éditable avec les dépendances de développement +- Met en place les hooks pre-commit +- Configure les outils de développement + +!!! note + + Vous devez créer et activer un environnement virtuel avant de lancer cette commande. + +## Configuration manuelle + +Si vous préférez la configuration manuelle ou si le Makefile ne fonctionne pas sur votre système : + +### 1. Cloner le dépôt + +
+ +```console +$ git clone https://github.com/bnbong/FastAPI-fastkit.git +$ cd FastAPI-fastkit +``` + +
+ +### 2. Créer l'environnement virtuel + +
+ +```console +$ python -m venv .venv +$ source .venv/bin/activate # On Windows: .venv\Scripts\activate +``` + +
+ +### 3. Installer les dépendances + +
+ +```console +# Installer le paquet en mode éditable avec les dépendances de développement +$ pip install -e ".[dev]" + +# Ou installer depuis les fichiers requirements +$ pip install -r requirements.txt +$ pip install -r requirements-dev.txt +``` + +
+ +### 4. Configurer les hooks pre-commit + +
+ +```console +$ pre-commit install +pre-commit installed at .git/hooks/pre-commit +``` + +
+ +### 5. Vérifier l'installation + +
+ +```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 ======================== +``` + +
+ +## Outils de développement + +L'environnement de développement inclut plusieurs outils pour maintenir la qualité du code : + +### Commandes en une ligne + +avec le Makefile : + +```console +$ make format lint +Running isort... +Running black... +Running mypy... +✅ All checks passed! +``` + +avec les scripts fournis : + +```console +$ ./scripts/format.sh +$ ./scripts/lint.sh +``` + +### Mise en forme du code + +**Black** — formateur de code : + +
+ +```console +$ black src/ tests/ +reformatted src/main.py +reformatted tests/test_cli.py +All done! ✨ 🍰 ✨ +``` + +
+ +**isort** — tri des imports : + +
+ +```console +$ isort src/ tests/ +Fixing import order in src/main.py +``` + +
+ +### Linting du code + +**mypy** — vérification de types : + +
+ +```console +$ mypy src/ +Success: no issues found in 12 source files +``` + +
+ +## Commandes Make disponibles + +Le Makefile du projet fournit des commandes pratiques pour les tâches courantes de développement : + +### Commandes de configuration + +| Commande | Description | +|---------|-------------| +| `make install` | Installer le paquet en mode production | +| `make install-dev` | Installer le paquet avec les dépendances de développement | +| `make install-test` | Installer le paquet pour les tests (désinstaller + réinstaller) | +| `make uninstall` | Désinstaller le paquet | +| `make clean` | Nettoyer les artefacts de build et les fichiers de cache | + +### Commandes de qualité de code + +| Commande | Description | +|---------|-------------| +| `make format` | Mettre en forme le code avec black et isort | +| `make format-check` | Vérifier la mise en forme sans modifier | +| `make lint` | Lancer toutes les vérifications de linting (isort, black, mypy) | + +### Commandes de tests + +| Commande | Description | +|---------|-------------| +| `make test` | Lancer tous les tests | +| `make test-verbose` | Lancer les tests avec une sortie détaillée | +| `make test-coverage` | Lancer les tests avec un rapport de couverture | +| `make coverage-report` | Générer un rapport de couverture détaillé (FORMAT=html/xml/json/all) | + +### Commandes d'inspection des modèles + +| Commande | Description | +|---------|-------------| +| `make inspect-templates` | Lancer l'inspection sur tous les modèles | +| `make inspect-templates-verbose` | Lancer l'inspection avec une sortie détaillée | +| `make inspect-template` | Inspecter un ou plusieurs modèles précis (paramètre TEMPLATES) | + +### Commandes de documentation + +| Commande | Description | +|---------|-------------| +| `make serve-docs` | Servir la documentation en local | +| `make build-docs` | Construire la documentation | + +### Commandes de traduction + +| Commande | Description | +|---------|-----------| +| `make translate` | Traduire la documentation (paramètres LANG, PROVIDER, MODEL) | + +### Exemples + +
+ +```console +# Format code and run all checks +$ make format lint +Running isort... +Running black... +Running mypy... +✅ All checks passed! + +# Run tests with coverage +$ 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% + +# Generate HTML coverage report +$ make coverage-report FORMAT=html +🌐 Opening HTML coverage report in browser... + +# Translate documentation to Korean +$ 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 +``` + +
+ +## Structure du projet + +Comprendre la structure du projet est crucial pour le développement : + +```bash +FastAPI-fastkit/ +├── src/ +│ ├── fastapi_fastkit/ +│ │ ├── __main__.py # Point d'entrée de l'application +│ │ ├── backend/ +│ │ │ ├── inspector.py # FastAPI-fastkit template inspector +│ │ │ ├── interactive/ +│ │ │ │ ├── config_builder.py # Construction de la configuration pour le mode interactif +│ │ │ │ ├── prompts.py # Invites du mode interactif +│ │ │ │ ├── selectors.py # Logique de sélection pour le mode interactif +│ │ │ │ └── validators.py # Validation des saisies utilisateur en mode interactif +│ │ │ ├── main.py # Point d'entrée de la logique backend +│ │ │ ├── package_managers/ +│ │ │ │ ├── base.py # Classe de base des gestionnaires de paquets +│ │ │ │ ├── factory.py # Fabrique des gestionnaires de paquets +│ │ │ │ ├── pdm_manager.py # Gestionnaire de paquets PDM +│ │ │ │ ├── pip_manager.py # Gestionnaire de paquets pip +│ │ │ │ ├── poetry_manager.py # Gestionnaire de paquets Poetry +│ │ │ │ └── uv_manager.py # Gestionnaire de paquets uv +│ │ │ ├── project_builder/ +│ │ │ │ ├── config_generator.py # Générateur de configuration pour le project builder +│ │ │ │ └── dependency_collector.py # Collecteur de dépendances pour le project builder +│ │ │ └── transducer.py # Transducteur pour le project builder +│ │ ├── cli.py # Point d'entrée principal du CLI FastAPI-fastkit +│ │ ├── core/ +│ │ │ ├── exceptions.py # Exception handling +│ │ │ └── settings.py # Settings configuration +│ │ ├── fastapi_project_template/ +│ │ │ ├── PROJECT_README_TEMPLATE.md # README de base des projets de template fastkit +│ │ │ ├── README.md # README des templates fastkit +│ │ │ ├── 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 # Configuration du logging +│ │ └── main.py # Point d'entrée principal de FastAPI-fastkit +│ └── logs +├── tests +│ ├── conftest.py # pytest configuration +│ ├── test_backends/ +│ ├── test_cli_operations/ +│ ├── test_core.py +│ ├── test_rich/ +│ ├── test_templates/ +│ └── test_utils.py +├── uv.lock +├── docs/ # Documentation +├── scripts/ # Development scripts +├── mkdocs.yml +├── overrides/ # mkdocs overrides +├── pdm.lock +├── pyproject.toml +├── requirements-docs.txt # requirements for documentation +├── requirements.txt # requirements for development +├── CHANGELOG.md +├── CITATION.cff +├── CODE_OF_CONDUCT.md +├── CONTRIBUTING.md +├── LICENSE +├── MANIFEST.in +├── Makefile +├── README.md +├── SECURITY.md +└── env.example # environment example(configures translation AI model env vars) +``` + +### Répertoires clés + +- **`src/fastapi_fastkit/`** — code source du paquet principal + - **`cli.py`** — point d'entrée principal du CLI + - **`backend/`** — logique backend centrale + - **`inspector.py`** — inspecteur de modèles + - **`interactive/`** — composants du mode interactif (invites, sélecteurs, validateurs) + - **`package_managers/`** — implémentations des gestionnaires de paquets (pip, uv, pdm, poetry) + - **`project_builder/`** — utilitaires de construction de projet + - **`transducer.py`** — transducer de modèle + - **`core/`** — configuration centrale et exceptions + - **`fastapi_project_template/`** — modèles de projet (fastapi-default, fastapi-async-crud, etc.) + - **`utils/`** — fonctions utilitaires partagées +- **`tests/`** — suite de tests + - **`test_backends/`** — tests propres au backend + - **`test_cli_operations/`** — tests d'opérations CLI + - **`test_templates/`** — tests du système de modèles +- **`docs/`** — documentation (MkDocs) + - guides utilisateur, tutoriels et référence d'API + +## Flux de travail de développement + +### 1. Créer une branche de fonctionnalité + +
+ +```console +$ git checkout -b feature/add-new-template +Switched to a new branch 'feature/add-new-template' +``` + +
+ +### 2. Apporter des changements + +Modifiez du code, ajoutez des fonctionnalités, corrigez des bogues… + +### 3. Lancer les tests et les vérifications + +
+ +```console +$ make dev-check +Running all quality checks... +Running all tests... +✅ All tests passed! +``` + +
+ +### 4. Committer les changements + +Les hooks pre-commit s'exécuteront automatiquement : + +
+ +```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. Pousser et créer une pull request + +
+ +```console +$ git push origin feature/add-new-template +$ gh pr create --title "Add new FastAPI template with authentication" +``` + +
+ +## Tests + +### Lancer les tests + +**Tous les tests :** + +
+ +```console +$ make test +# or +$ python -m pytest +``` + +
+ +**Fichier de test précis :** + +
+ +```console +$ python -m pytest tests/test_cli.py -v +``` + +
+ +**Avec couverture :** + +
+ +```console +$ make test-coverage +# or +$ python -m pytest --cov=src --cov-report=html +``` + +
+ +### Écrire des tests + +Lorsque vous ajoutez de nouvelles fonctionnalités, incluez toujours des tests : + +```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() +``` + +### Catégories de tests + +**Tests unitaires** — tester des fonctions et des classes individuelles : + +```python +def test_validate_project_name(): + assert validate_project_name("valid-name") is True + assert validate_project_name("invalid name!") is False +``` + +**Tests d'intégration** — tester les interactions entre commandes : + +```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() +``` + +**Tests de bout en bout** — tester des flux complets : + +```python +def test_full_project_creation_workflow(tmp_path): + # Create project + result = runner.invoke(cli, ['init'], input='...') + assert result.exit_code == 0 + + # Add route + result = runner.invoke(cli, ['addroute', 'test-project', 'users']) + assert result.exit_code == 0 + + # Verify files exist + assert (tmp_path / "test-project" / "src" / "api" / "routes" / "users.py").exists() +``` + +## Documentation + +### Servir la documentation en local + +
+ +```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/ +``` + +
+ +### Construire la documentation + +
+ +```console +$ make build-docs +INFO - Building documentation... +INFO - Documentation built in 0.43 seconds +``` + +
+ +### Rédiger la documentation + +La documentation est rédigée en Markdown et construite avec MkDocs. Voici un exemple de structure : + +**Modèle de guide de fonctionnalité :** + +````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. +```` + +Pour une référence détaillée sur l'utilisation de `mkdocs-material`, consultez la [documentation mkdocs-material](https://squidfunk.github.io/mkdocs-material/reference/admonitions/). + +## Directives de style de code + +### Style de code Python + +Suivez [PEP 8](https://www.python.org/dev/peps/pep-0008/) avec ces règles spécifiques : + +- **Longueur de ligne** : 88 caractères (par défaut de Black) +- **Imports** : organisés avec isort +- **Annotations de type** : requises pour toutes les fonctions publiques +- **Docstrings** : style Google pour toutes les API publiques + +### Exemple + +```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}") + + # Implementation here... + return created_files +``` + +## Variables d'environnement + +Pour le développement, vous pouvez définir ces variables d'environnement : + +| Variable | Description | Défaut | +|----------|-------------|---------| +| `FASTKIT_DEBUG` | Activer la journalisation de débogage | `False` | +| `FASTKIT_DEV_MODE` | Activer les fonctionnalités de développement | `False` | +| `FASTKIT_TEMPLATE_DIR` | Répertoire de modèles personnalisés | Modèles intégrés | +| `FASTKIT_CONFIG_DIR` | Répertoire de configuration | `~/.fastkit` | +| `TRANSLATION_API_KEY` | Clé d'API de traduction (mettez un PAT GitHub si vous utilisez le [fournisseur de modèles IA Github](https://github.com/marketplace/models/azure-openai)) | `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', ...] +``` + +
+ +Pour les autres paramètres de variables d'environnement, référez-vous au module [@settings.py](https://github.com/bnbong/FastAPI-fastkit/blob/main/src/fastapi_fastkit/core/settings.py). + +## Dépannage + +### Problèmes courants + +**1. Les hooks pre-commit échouent :** + +
+ +```console +$ git commit -m "Fix bug" +black....................................................................Failed +hookid: black + +Files were modified by this hook. Additional output: + +would reformat src/cli.py +``` + +
+ +**Solution :** lancez les formateurs et committez à nouveau : + +
+ +```console +$ make format +$ git add . +$ git commit -m "Fix bug" +``` + +
+ +**2. Les tests échouent sur différentes versions de Python :** + +**Solution :** utilisez tox pour tester plusieurs versions de Python : + +
+ +```console +$ pip install tox +$ tox +py38: commands succeeded +py39: commands succeeded +py310: commands succeeded +py311: commands succeeded +py312: commands succeeded +``` + +
+ +**3. Erreurs d'import en développement :** + +**Solution :** installez le paquet en mode éditable : +
+ +```console +$ pip install -e . +``` + +
+ +### Obtenir de l'aide + +- **[GitHub Issues](https://github.com/bnbong/FastAPI-fastkit/issues)** : signaler des bogues et demander des fonctionnalités +- **[GitHub Discussions](https://github.com/bnbong/FastAPI-fastkit/discussions)** : poser des questions et partager des idées +- **Documentation** : consultez le [Guide de l'utilisateur](../user-guide/installation.md) + +## Directives de contribution + +### Avant de soumettre une PR + +1. **Lancez toutes les vérifications :** `make dev-check` +2. **Mettez à jour la documentation** si nécessaire +3. **Ajoutez des tests** pour les nouvelles fonctionnalités +4. **Suivez les conventions de messages de commit** + +### Format de message de commit + +``` +type(scope): brief description + +Longer description if needed + +Fixes #123 +``` + +**Types :** + +- `feat` : nouvelle fonctionnalité +- `fix` : correction de bogue +- `docs` : changements de documentation +- `style` : changements de style de code +- `refactor` : refactoring de code +- `test` : ajout / modification de tests +- `chore` : tâches de maintenance + +**Exemples :** + +``` +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 +``` + +## Processus de release + +Pour les mainteneurs, le processus de release est : + +1. **Mettre à jour la version** dans `setup.py` et `__init__.py` +2. **Mettre à jour CHANGELOG.md** +3. **Créer une PR de release** +4. **Tagger la release** après la fusion +5. **GitHub Actions** construit et publie automatiquement + +
+ +```console +$ git tag v1.2.0 +$ git push origin v1.2.0 +``` + +
+ +## Étapes suivantes + +Maintenant que votre environnement de développement est en place : + +1. **[Explorez le code](https://github.com/bnbong/FastAPI-fastkit/tree/main/src/fastapi_fastkit)** pour comprendre l'architecture +2. **Lancez la suite de tests** pour vérifier que tout fonctionne +3. **Choisissez une [issue](https://github.com/bnbong/FastAPI-fastkit/issues)** sur GitHub à traiter +4. **Rejoignez les [discussions](https://github.com/bnbong/FastAPI-fastkit/discussions)** pour échanger avec les autres contributeurs + +Bon développement ! 🚀 + +!!! tip "Astuces de développement" + - Utilisez `make dev-check` avant de committer + - Écrivez les tests d'abord (approche TDD) + - Gardez les commits petits et ciblés + - Mettez à jour la documentation avec les nouvelles fonctionnalités diff --git a/docs/fr/contributing/template-creation-guide.md b/docs/fr/contributing/template-creation-guide.md new file mode 100644 index 0000000..61932ee --- /dev/null +++ b/docs/fr/contributing/template-creation-guide.md @@ -0,0 +1,574 @@ +# Guide de création de modèles FastAPI + +Un guide complet pour ajouter de nouveaux modèles de projet FastAPI à FastAPI-fastkit. + +## 🎯 Vue d'ensemble + +L'ajout d'un nouveau modèle suit un processus en 5 étapes : + +1. **📋 Planification et conception** — définir l'objectif et la structure du modèle +2. **🏗️ Implémentation du modèle** — créer la structure et les fichiers requis +3. **🔍 Validation locale** — valider le modèle avec l'inspecteur +4. **📚 Documentation** — rédiger le README et le guide d'utilisation +5. **🚀 Soumission et relecture** — créer la PR et relecture par la communauté + +## 📋 Étape 1 : Planification et conception + +### Définir l'objectif du modèle + +Avant de créer un nouveau modèle, répondez à ces questions : + +- **Quelle est la valeur unique de ce modèle ?** +- **En quoi se distingue-t-il des modèles existants ?** +- **Quel groupe d'utilisateurs est visé ?** +- **Quelle pile technique sera incluse ?** + +### Convention de nommage des modèles + +``` +fastapi-{purpose}-{stack} +``` + +Exemples : + +- `fastapi-microservice` (modèle microservice) +- `fastapi-graphql` (modèle d'intégration GraphQL) +- `fastapi-auth-jwt` (modèle d'authentification JWT) + +### Planification de la pile technique + +Définissez à l'avance les principales technologies à inclure : + +```yaml +# Example: fastapi-microservice template +core_dependencies: + - fastapi + - uvicorn + - pydantic + - pydantic-settings + +additional_features: + - sqlalchemy (ORM) + - alembic (migrations) + - redis (caching) + - celery (background tasks) + - pytest (testing) + +development_tools: + - black (code formatting) + - isort (import sorting) + - mypy (vérification des types) + - pre-commit (hooks Git) +``` + +## 🏗️ Étape 2 : Implémentation du modèle + +### Structure de répertoires requise + +``` +fastapi-{template-name}/ +├── src/ # Code source de l'application +│ ├── main.py-tpl # ✅ Point d'entrée FastAPI (obligatoire) +│ ├── __init__.py-tpl +│ ├── api/ # Routeurs API +│ │ ├── __init__.py-tpl +│ │ ├── api.py-tpl # Routeur API principal +│ │ └── routes/ # Routes individuelles +│ │ ├── __init__.py-tpl +│ │ └── items.py-tpl # Exemple de route +│ ├── core/ # Configuration centrale +│ │ ├── __init__.py-tpl +│ │ └── config.py-tpl # Gestion des paramètres +│ ├── crud/ # Logique CRUD +│ │ ├── __init__.py-tpl +│ │ └── items.py-tpl +│ ├── schemas/ # Modèles Pydantic +│ │ ├── __init__.py-tpl +│ │ └── items.py-tpl +│ └── utils/ # Fonctions utilitaires +│ ├── __init__.py-tpl +│ └── helpers.py-tpl +├── tests/ # ✅ Tests (obligatoires) +│ ├── __init__.py-tpl +│ ├── conftest.py-tpl # Configuration pytest +│ └── test_items.py-tpl # Exemples de tests +├── scripts/ # Scripts +│ ├── format.sh-tpl # Formatage du code +│ ├── lint.sh-tpl # Vérifications de style +│ ├── run-server.sh-tpl # Lancement du serveur +│ └── test.sh-tpl # Exécution des tests +├── pyproject.toml-tpl # ✅ Métadonnées principales (PEP 621, recommandé) +├── setup.py-tpl # 🟡 Métadonnées legacy (acceptées pour la rétrocompatibilité) +├── requirements.txt-tpl # 🟡 Optionnel si pyproject déclare déjà les dépendances +├── setup.cfg-tpl # Configuration des outils de développement +├── README.md-tpl # ✅ Documentation du projet (obligatoire) +├── .env-tpl # Modèle de variables d'environnement +└── .gitignore-tpl # Fichier d'exclusion Git +``` + +**Fichiers minimaux requis.** Un modèle doit fournir : + +- un répertoire `tests/` +- un fichier `README.md-tpl` +- au moins un fichier de métadonnées : `pyproject.toml-tpl` (préféré, PEP 621) ou `setup.py-tpl` (legacy, toujours accepté) +- une déclaration de `fastapi` comme dépendance dans au moins l'un de : `pyproject.toml-tpl` `[project].dependencies`, `requirements.txt-tpl`, ou `setup.py-tpl` `install_requires` + +`requirements.txt-tpl` n'est plus strictement requis quand `pyproject.toml-tpl` déclare `[project].dependencies`. Les modèles modernes DEVRAIENT adopter `pyproject.toml-tpl` comme fichier de métadonnées principal. + +### Guide d'écriture des fichiers + +#### 1. Écrire main.py-tpl + +```python +""" +Point d'entrée de l'application FastAPI + +Ce fichier correspond à l'application principale du projet créé avec FastAPI-fastkit. +""" +from fastapi import FastAPI +from fastapi.middleware.cors import CORSMiddleware + +from api.api import api_router +from core.config import settings + +# Create FastAPI app (required for inspector validation) +app = FastAPI( + title="", + description="Project created with FastAPI-fastkit", + version="1.0.0", +) + +# CORS middleware configuration +app.add_middleware( + CORSMiddleware, + allow_origins=["*"], + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + +# Register API router +app.include_router(api_router, prefix="/api/v1") + +@app.get("/") +async def root(): + """Root endpoint""" + return {"message": "Hello from !"} + +@app.get("/health") +async def health_check(): + """Health check endpoint""" + return {"status": "healthy"} + +if __name__ == "__main__": + import uvicorn + uvicorn.run(app, host="0.0.0.0", port=8000) +``` + +#### 2. Écrire pyproject.toml-tpl (préféré) + +Les modèles modernes doivent déclarer leurs métadonnées et leurs dépendances avec un `pyproject.toml-tpl` PEP 621. Au minimum, le fichier doit exposer une section `[project]` avec `name`, `version`, une `description` et une liste `dependencies` qui inclut `fastapi`. Les modèles doivent aussi porter deux marqueurs d'identité FastAPI-fastkit afin que `is_fastkit_project()` puisse distinguer les projets générés des projets FastAPI non liés dans l'espace de travail de l'utilisateur : + +- préfixe `[FastAPI-fastkit templated]` dans `description` +- une table dédiée `[tool.fastapi-fastkit]` avec `managed = true` + +La détection accepte n'importe lequel des deux marqueurs (la comparaison est insensible à la casse). L'injection de métadonnées ajoutera les deux au moment de la génération du projet si un modèle les omet, mais les auteurs devraient les inclure explicitement. + +```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. Écrire requirements.txt-tpl (optionnel) + +Optionnel quand `pyproject.toml-tpl` déclare `[project].dependencies`. Reste utile pour les modèles qui préfèrent des flux de travail uniquement `pip`. + +```txt +# FastAPI core dependencies (required) +fastapi==0.104.1 +uvicorn[standard]==0.24.0 + +# Data validation +pydantic==2.5.0 +pydantic-settings==2.1.0 + +# Environment variable management +python-dotenv==1.0.0 + +# Database (if needed) +sqlalchemy==2.0.23 +alembic==1.13.0 + +# Development tools +pytest==7.4.3 +pytest-asyncio==0.21.1 +httpx==0.25.2 + +# Code quality +black==23.11.0 +isort==5.12.0 +mypy==1.7.1 +``` + +#### 4. Écrire setup.py-tpl (legacy — optionnel quand pyproject est présent) + +Conservé pour les modèles legacy. Les nouveaux modèles n'ont pas besoin de ce fichier s'ils embarquent `pyproject.toml-tpl`. + +```python +""" + package setup + +Project created with FastAPI-fastkit. +""" +from setuptools import find_packages, setup + +# Dependencies list (type annotation required) +install_requires: list[str] = [ + "fastapi>=0.104.0", + "uvicorn[standard]>=0.24.0", + "pydantic>=2.5.0", + "pydantic-settings>=2.1.0", + "python-dotenv>=1.0.0", +] + +setup( + name="", + version="1.0.0", + description="[FastAPI-fastkit templated] ", # Identity marker used by is_fastkit_project() + long_description=open("README.md").read(), + long_description_content_type="text/markdown", + author="", + author_email="", + packages=find_packages(), + install_requires=install_requires, + python_requires=">=3.8", + classifiers=[ + "Development Status :: 4 - Beta", + "Intended Audience :: Developers", + "License :: OSI Approved :: MIT License", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + ], +) +``` + +#### 5. Écrire les fichiers de test + +```python +# tests/test_items.py-tpl +""" +Items API test module +""" +import pytest +from fastapi.testclient import TestClient +from main import app + +client = TestClient(app) + +def test_read_root(): + """Test root endpoint""" + response = client.get("/") + assert response.status_code == 200 + assert "message" in response.json() + +def test_health_check(): + """Tester le point de contrôle de santé""" + response = client.get("/health") + assert response.status_code == 200 + assert response.json() == {"status": "healthy"} + +def test_create_item(): + """Test item creation""" + 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(): + """Test reading items list""" + response = client.get("/api/v1/items/") + assert response.status_code == 200 + assert isinstance(response.json(), list) +``` + +## 🔍 Étape 3 : Validation locale + +### Exécuter les scripts de validation automatisés + +Une fois votre nouveau modèle prêt, validez-le avec ces commandes : + +```bash +# Validate all templates +make inspect-templates + +# Validate specific template only +make inspect-template TEMPLATES="fastapi-your-template" + +# Validate with verbose output +python scripts/inspect-templates.py --templates "fastapi-your-template" --verbose +``` + +!!! note + + Lorsque vous soumettez une PR, le workflow **Template PR Inspection** se déclenche automatiquement et valide vos changements de modèle. Vous recevrez le retour directement sur votre PR. + +### Liste de vérification + +L'inspecteur valide automatiquement les éléments suivants : + +#### ✅ Validation de la structure des fichiers + +- [ ] Le répertoire `tests/` existe +- [ ] Le fichier `README.md-tpl` existe +- [ ] Au moins l'un de `pyproject.toml-tpl` (préféré) ou `setup.py-tpl` (legacy) existe + +#### ✅ Validation des extensions de fichiers + +- [ ] Tous les fichiers Python utilisent l'extension `.py-tpl` +- [ ] Aucun fichier en `.py` n'existe + +#### ✅ Validation des dépendances + +- [ ] `fastapi` est déclaré dans au moins l'un de : + - [ ] `pyproject.toml-tpl` sous `[project].dependencies` (préféré) + - [ ] `requirements.txt-tpl` + - [ ] `setup.py-tpl` sous `install_requires` + +#### ✅ Validation de l'implémentation FastAPI + +- [ ] L'import `FastAPI` existe dans `main.py-tpl` +- [ ] La création d'application du type `app = FastAPI()` existe dans `main.py-tpl` + +#### ✅ Validation de l'exécution des tests + +- [ ] La création de l'environnement virtuel réussit +- [ ] L'installation des dépendances réussit +- [ ] Tous les tests pytest passent + +#### ✅ Tests automatisés des modèles + +FastAPI-fastkit inclut des **tests automatisés des modèles** qui exécutent des tests complets pour tous les modèles : + +**Couverture des tests :** + +- ✅ Processus de création de modèle +- ✅ Injection des métadonnées de projet +- ✅ Mise en place de l'environnement virtuel +- ✅ Installation des dépendances (tous les gestionnaires de paquets) +- ✅ Validation de la structure basique du projet +- ✅ Identification du projet FastAPI + +**Exécution des tests :** +```console +# Test all templates automatically +$ pytest tests/test_templates/test_all_templates.py -v + +# Test specific template +$ pytest tests/test_templates/test_all_templates.py::TestAllTemplates::test_template_creation[your-template-name] -v +``` + +**Découverte des tests de modèle :** +Les nouveaux modèles sont **automatiquement découverts** et testés sans configuration manuelle : + +1. ✅ **Zéro configuration** : ajoutez un modèle → tests automatiques +2. ✅ **Tests cohérents** : mêmes standards de qualité pour tous les modèles +3. ✅ **Gestionnaires de paquets multiples** : tests avec UV, PDM, Poetry et PIP +4. ✅ **Validation complète** : vérifications de structure, métadonnées et fonctionnalités + +**Ce que cela signifie pour vous :** + +- 🚀 **Aucun fichier de test supplémentaire dans la source principale de `FastAPI-fastkit`** : votre modèle est testé automatiquement +- ⚡ **Développement plus rapide** : concentrez-vous sur le contenu du modèle, pas sur la configuration des tests +- 🛡️ **Assurance qualité** : tests cohérents pour tous les modèles +- 🔄 **Intégration CI/CD** : tests automatiques dans les pull requests + +**Tests manuels toujours requis :** + +- 🧪 **Fonctionnalités propres au modèle** : logique métier et fonctionnalités personnalisées +- 🔧 **Tests d'intégration** : services externes et flux de travail complexes +- 📱 **Scénarios de bout en bout** : flux utilisateurs complets + +**Bonnes pratiques de test :** +```console +# 1. Test your template locally +$ fastkit startdemo your-template-name + +# 2. Run automated tests +$ pytest tests/test_templates/test_all_templates.py::TestAllTemplates::test_template_creation[your-template-name] -v + +# 3. Test with different package managers +$ fastkit startdemo your-template-name --package-manager poetry +$ fastkit startdemo your-template-name --package-manager pdm +$ fastkit startdemo your-template-name --package-manager uv +``` + +### Liste de vérification manuelle + +En complément de la validation automatisée, vérifiez manuellement les éléments suivants : + +#### 🔧 Qualité du code + +- [ ] Le code respecte le guide de style PEP 8 +- [ ] Utilisation appropriée des annotations de type +- [ ] Noms de variables et de fonctions explicites +- [ ] Commentaires et docstrings appropriés + +#### 🏗️ Architecture + +- [ ] Séparation des préoccupations (séparation API, logique métier, accès aux données) +- [ ] Conception de composants réutilisables +- [ ] Structure évolutive +- [ ] Bonnes pratiques de sécurité appliquées + +#### 📚 Documentation + +- [ ] README.md-tpl respecte le format de PROJECT_README_TEMPLATE.md +- [ ] Méthodes d'installation et d'exécution précisées +- [ ] Documentation de l'API (OpenAPI/Swagger) +- [ ] Explication des variables d'environnement + +## 📚 Étape 4 : Documentation + +### Rédiger README.md-tpl + +Rédigez en suivant le guide [PROJECT_README_TEMPLATE.md](https://github.com/bnbong/FastAPI-fastkit/blob/main/src/fastapi_fastkit/fastapi_project_template/PROJECT_README_TEMPLATE.md). + +### Rédiger la documentation de description du modèle + +Ajoutez une description de votre nouveau modèle à `src/fastapi_fastkit/fastapi_project_template/README.md` : + +```markdown +## fastapi-your-template + +Write a brief description and use cases for your new template here. + +### Features: +- Feature 1 +- Feature 2 +- Feature 3 + +### Use Cases: +- Use case 1 +- Use case 2 +``` + +## 🚀 Étape 5 : Soumission et relecture + +### Liste de vérification avant création de la PR + +- [ ] Toute la validation automatisée est passée (`make inspect-templates`) +- [ ] La mise en forme du code est terminée (`make format`) +- [ ] Les contrôles de linting sont passés (`make lint`) +- [ ] Tous les tests sont passés (`make test`) +- [ ] La documentation est complète +- [ ] Les directives de CONTRIBUTING.md sont respectées + +### Titre et description de la 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 +``` + +### Processus de relecture + +1. **Validation automatisée** : GitHub Actions valide automatiquement le modèle + - **Template PR Inspection** : exécute `inspect-changed-templates.py` sur les PR qui modifient des modèles + - **Inspection hebdomadaire** : validation complète des modèles chaque mercredi +2. **Relecture de code** : les mainteneurs et la communauté relisent le code +3. **Tests** : le modèle est testé dans divers environnements +4. **Relecture de la documentation** : vérification de l'exactitude et de la complétude de la documentation +5. **Approbation et fusion** : fusion dans la branche main lorsque toutes les exigences sont satisfaites + +!!! note + + Vous recevrez des commentaires automatiques de PR avec les résultats de validation. Vérifiez-les avant de demander une relecture ! + +## 🎯 Bonnes pratiques + +### Considérations de sécurité + +- Gérez les informations sensibles avec des variables d'environnement +- Configuration CORS correcte +- Validation des données d'entrée +- Prévention des injections SQL + +### Optimisation des performances + +- Tirer parti du traitement asynchrone +- Optimiser les requêtes de base de données +- Stratégies de mise en cache appropriées +- Paramètres de compression des réponses + +### Maintenabilité + +- Structure de code claire +- Couverture de tests complète +- Documentation détaillée +- Mise en place de journalisation et de supervision + +## 🆘 Besoin d'aide ? + +- 📖 [Guide de configuration du développement](development-setup.md) +- 📋 [Directives de code](code-guidelines.md) +- 💬 [GitHub Discussions](https://github.com/bnbong/FastAPI-fastkit/discussions) +- 📧 [Contacter le mainteneur](mailto:bbbong9@gmail.com) + +Ajouter un nouveau modèle est une superbe contribution à la communauté FastAPI-fastkit. Vos idées et vos efforts seront d'une grande aide pour d'autres développeurs ! 🚀 diff --git a/docs/fr/contributing/translation-guide.md b/docs/fr/contributing/translation-guide.md new file mode 100644 index 0000000..7bd213d --- /dev/null +++ b/docs/fr/contributing/translation-guide.md @@ -0,0 +1,367 @@ +# Guide de traduction + +Ce guide explique comment contribuer aux traductions de la documentation de FastAPI-fastkit. + +## Source de vérité et politique de traduction + +> **L'anglais (`en`) est la référence canonique** pour la documentation de FastAPI-fastkit. Toutes les autres locales sont des cibles de traduction qui peuvent avoir un décalage d'une version complète ou de quelques pages par rapport à l'anglais. +> +> Si une page traduite contredit la page anglaise, **faites confiance à la page anglaise** jusqu'à ce que la traduction rattrape son retard. Les traductions sont livrées dans l'état d'avancement atteint par les contributeurs — une couverture partielle est normale et attendue. + +Le pendant côté utilisateur de cette politique est la page [État des traductions](../reference/translation-status.md), qui liste la complétude réelle de chaque locale et la manière dont MkDocs rend les pages qui n'ont pas encore été traduites (en résumé : elles retombent sur l'anglais). + +Le `CHANGELOG.md` à la racine du dépôt reste également en anglais comme historique de versions canonique. Si une locale expose une page `changelog.md`, cette page doit pointer vers le changelog canonique en anglais ou l'inclure, plutôt que de maintenir une copie traduite séparée, à moins que la politique du projet ne change plus tard. + +Lorsque vous contribuez à une traduction, mettez aussi à jour le tableau de la page d'état pour que les utilisateurs sachent ce qui est disponible sans avoir à deviner depuis le sélecteur de langue. + +## Vue d'ensemble + +FastAPI-fastkit utilise un système de traduction automatisé propulsé par l'IA pour traduire la documentation dans plusieurs langues. Le système : + +- Lit la documentation source en anglais +- Traduit le contenu via des API IA (OpenAI ou Anthropic) +- Enregistre les traductions dans des répertoires propres à chaque langue +- Crée des pull requests GitHub pour relecture + +L'automatisation produit un point de départ ; une relecture humaine reste requise avant la fusion. Les traductions générées par IA doivent être marquées comme « brouillon » dans leurs PR et relues par un locuteur courant avant d'atterrir. + +## Langues prises en charge + +Voici les locales que le site de documentation **compile** actuellement. La configuration de cible de build seule **ne signifie pas** que les pages d'une locale sont traduites — consultez [État des traductions](../reference/translation-status.md) pour la complétude réelle par locale. + +- 🇰🇷 Coréen (ko) +- 🇯🇵 Japonais (ja) +- 🇨🇳 Chinois (zh) +- 🇪🇸 Espagnol (es) +- 🇫🇷 Français (fr) +- 🇩🇪 Allemand (de) + +## Prérequis + +### 1. Installer les dépendances de traduction + +```bash +# Install using pip +pip install openai anthropic + +# Or using pdm +pdm install -G translation +``` + +### 2. Configurer les clés d'API + +Vous avez besoin d'une clé d'API d'OpenAI ou d'Anthropic : + +```bash +# For OpenAI +export TRANSLATION_API_KEY="sk-..." + +# Or for Anthropic +export TRANSLATION_API_KEY="sk-ant-..." +``` + +### 3. Installer GitHub CLI (optionnel) + +Pour la création automatique de 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 + +# Authenticate +gh auth login +``` + +## Utilisation + +### Avec les commandes Make (recommandé) + +La façon la plus simple d'exécuter les traductions : + +```bash +# Translate all docs to all languages +make translate + +# Translate to specific language +make translate LANG=ko + +# Specify API provider and model +make translate LANG=ko PROVIDER=openai MODEL=gpt-4 +make translate LANG=ko PROVIDER=github MODEL=gpt-4o-mini +``` + +### Avec le script directement + +#### Traduire toute la documentation + +Traduire toute la documentation dans toutes les langues prises en charge : + +```bash +python scripts/translate.py --api-provider openai +``` + +### Traduire vers une langue spécifique + +Traduire uniquement vers le coréen : + +```bash +python scripts/translate.py --target-lang ko --api-provider openai +``` + +### Traduire des fichiers spécifiques + +Traduire uniquement certains fichiers de documentation : + +```bash +python scripts/translate.py \ + --target-lang ko \ + --files user-guide/installation.md user-guide/quick-start.md \ + --api-provider openai +``` + +### Sauter la création de PR + +Traduire sans créer de PR GitHub : + +```bash +python scripts/translate.py --target-lang ko --no-pr --api-provider openai +``` + +### Utiliser Anthropic Claude + +Utiliser Claude d'Anthropic au lieu d'OpenAI : + +```bash +python scripts/translate.py \ + --target-lang ko \ + --api-provider anthropic \ + --api-key "sk-ant-..." +``` + +## Structure des répertoires + +Après la traduction, la structure de la documentation ressemblera à ceci : + +``` +docs/ +├── en/ # English (original) +│ ├── index.md +│ ├── user-guide/ +│ │ ├── installation.md +│ │ ├── quick-start.md +│ │ └── ... +│ ├── tutorial/ +│ └── ... +├── ko/ # Korean +│ ├── index.md +│ ├── user-guide/ +│ └── ... +├── ja/ # Japanese +├── zh/ # Chinese +├── es/ # Spanish +├── fr/ # French +├── de/ # German +├── css/ # Shared assets +├── js/ # Shared assets +└── img/ # Shared assets +``` + +## Flux de travail de traduction + +### 1. Rédiger la documentation en anglais + +Toute la documentation doit d'abord être rédigée en anglais dans le répertoire `docs/` : + +```bash +# Create new documentation +vim docs/user-guide/new-feature.md +``` + +### 2. Lancer la traduction + +Une fois la documentation anglaise terminée, lancez le script de traduction : + +```bash +python scripts/translate.py --target-lang ko +``` + +### 3. Relire la pull request + +Le script créera une pull request avec les traductions. Relisez la PR : + +1. Vérifiez que la mise en forme markdown est préservée +2. Vérifiez que les termes techniques sont correctement traités +3. Assurez-vous que les exemples de code restent inchangés +4. Vérifiez les particularités propres à la langue + +### Politique sur le changelog + +- Conservez le `CHANGELOG.md` à la racine du dépôt en anglais. +- N'ouvrez pas de PR de traduction dont l'objectif est de réécrire l'historique de versions dans une autre langue au sein du changelog racine. +- Si une locale a besoin d'une page de changelog, traitez `docs//changelog.md` comme une enveloppe ou un point d'entrée vers le changelog canonique en anglais. + +### 4. Approuver et fusionner (pour les mainteneurs) + +Une fois la traduction vérifiée : + +```bash +gh pr review --approve +gh pr merge +``` + +### 5. Déployer la documentation + +Le site de documentation sera automatiquement reconstruit avec les nouvelles traductions. + +## Configuration de la traduction + +Modifiez `scripts/translation_config.json` pour personnaliser : + +```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" + } +} +``` + +## Bonnes pratiques + +### Pour la documentation source + +1. **Utilisez un langage clair** : rédigez un anglais clair et simple qui se traduit bien +2. **Terminologie cohérente** : utilisez des termes techniques cohérents +3. **Blocs de code propres** : précisez toujours la langue des blocs de code +4. **Vérification des liens** : assurez-vous que tous les liens internes utilisent des chemins relatifs + +### Pour la relecture des traductions + +1. **Termes techniques** : vérifiez que les termes techniques sont appropriés à la langue cible +2. **Contexte culturel** : vérifiez si les exemples nécessitent une localisation +3. **Mise en forme** : assurez-vous que toute la mise en forme markdown est préservée +4. **Intégrité du code** : vérifiez que les blocs de code sont inchangés + +## Dépannage + +### Limites de débit de l'API + +Si vous atteignez les limites de débit de l'API, traduisez par lots plus petits : + +```bash +# Translate only user guide +python scripts/translate.py \ + --target-lang ko \ + --files user-guide/*.md +``` + +### Problèmes de qualité de traduction + +Si les traductions sont de mauvaise qualité : + +1. Vérifiez que votre clé d'API est valide +2. Essayez un autre fournisseur d'IA +3. Découpez les documents complexes en sections plus petites +4. Relisez et éditez manuellement la traduction + +### Échec de la PR GitHub + +Si la création de PR échoue : + +```bash +# Translate without PR +python scripts/translate.py --target-lang ko --no-pr + +# Manually create 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" +``` + +## Traduction manuelle + +Vous pouvez aussi traduire manuellement : + +1. Copiez le fichier anglais dans le répertoire de la langue cible : +```bash +mkdir -p docs/ko/user-guide +cp docs/en/user-guide/installation.md docs/ko/user-guide/installation.md +``` + +2. Éditez le fichier dans votre éditeur favori +3. Committez et créez une PR + +## Changement de langue + +Le site de documentation inclut un sélecteur de langue dans la navigation supérieure. Les utilisateurs peuvent : + +1. Cliquer sur le sélecteur de langue +2. Choisir leur langue préférée +3. Naviguer dans la documentation traduite + +## Contribuer de nouvelles langues + +Pour ajouter une nouvelle langue : + +1. Modifiez `scripts/translation_config.json` : +```json +{ + "code": "pt", + "name": "Portuguese", + "native_name": "Português", + "enabled": true +} +``` + +2. Mettez à jour `mkdocs.yml` : +```yaml +- locale: pt + name: Português + build: true +``` + +3. Lancez la traduction : +```bash +python scripts/translate.py --target-lang pt +``` + +## Besoin d'aide ? + +- **Issues** : signalez les problèmes de traduction sur [GitHub Issues](https://github.com/bnbong/FastAPI-fastkit/issues) +- **Discussions** : posez vos questions dans [GitHub Discussions](https://github.com/bnbong/FastAPI-fastkit/discussions) +- **Contribution** : consultez [CONTRIBUTING.md](https://github.com/bnbong/FastAPI-fastkit/blob/main/CONTRIBUTING.md) + +## Standards de qualité des traductions + +Toutes les traductions doivent respecter ces standards : + +- ✅ Préserver toute la mise en forme markdown +- ✅ Laisser les blocs de code inchangés +- ✅ Maintenir une terminologie technique appropriée +- ✅ Utiliser une grammaire et une orthographe correctes +- ✅ Suivre les conventions propres à la langue +- ✅ Vérifier que tous les liens fonctionnent + +Merci de contribuer aux traductions de FastAPI-fastkit ! 🌍 diff --git a/docs/fr/index.md b/docs/fr/index.md new file mode 100644 index 0000000..ba94aa9 --- /dev/null +++ b/docs/fr/index.md @@ -0,0 +1,577 @@ +

+ FastAPI-fastkit +

+

+FastAPI-fastkit : kit de démarrage rapide et facile à utiliser pour les nouveaux utilisateurs de Python et FastAPI +

+

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

+ +--- + +Ce projet a été créé pour accélérer la configuration de l'environnement de développement nécessaire au développement d'applications web basées sur Python pour les nouveaux utilisateurs de Python et de [FastAPI](https://github.com/fastapi/fastapi). + +Ce projet s'inspire de l'`initializer` de SpringBoot et de l'opération CLI `django-admin` de Python Django. + +!!! info "État des traductions" + L'anglais sert de référence principale pour cette documentation. Les autres langues du sélecteur peuvent être incomplètes ou afficher, page par page, la version anglaise. Consultez [État des traductions](reference/translation-status.md) pour connaître le niveau réel de traduction de chaque locale. + +## Fonctionnalités clés + +- **⚡ Création immédiate de projets FastAPI** : création en quelques secondes d'espaces de travail et de projets FastAPI depuis la CLI, inspirée de l'expérience `django-admin` de [Python Django](https://github.com/django/django) +- **✨ Assistant interactif de création** : choix pas à pas des bases de données, de l'authentification, du cache, de la supervision et d'autres fonctionnalités, avec génération automatique du code correspondant +- **🎨 Sorties CLI plus élégantes** : expérience CLI soignée grâce à la [bibliothèque rich](https://github.com/Textualize/rich) +- **📋 Modèles de projet FastAPI fondés sur les standards** : tous les modèles de FastAPI-fastkit s'appuient sur les standards Python et les usages courants de FastAPI +- **🔍 Assurance qualité automatisée des modèles** : des tests automatisés hebdomadaires garantissent que tous les modèles restent fonctionnels et à jour +- **🚀 Plusieurs modèles de projet** : choisissez parmi divers modèles préconfigurés pour différents cas d'usage (async CRUD, Docker, PostgreSQL, etc.) +- **📦 Prise en charge de plusieurs gestionnaires de paquets** : choisissez votre gestionnaire de paquets Python préféré (pip, uv, pdm, poetry) pour la gestion des dépendances + +## Installation + +Installez `FastAPI-fastkit` dans votre environnement Python. + +
+ +```console +$ pip install FastAPI-fastkit +---> 100% +``` + +
+ + +## Utilisation + +### Créer immédiatement un nouvel espace de travail FastAPI + +Vous pouvez désormais démarrer un nouveau projet FastAPI très rapidement avec FastAPI-fastkit ! + +Créez immédiatement un nouvel espace de travail pour votre projet FastAPI avec : + +
+ +```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 │ +╰──────────────────────────────────────────────────────╯ +``` + +
+ +Cette commande crée un nouvel espace de travail FastAPI avec son environnement virtuel Python. + +### Créer un projet en mode interactif ✨ NOUVEAU ! + +Pour les projets plus complexes, utilisez le **mode interactif** afin de construire votre application FastAPI étape par étape, avec une sélection intelligente des fonctionnalités : + +
+ +```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! │ +╰───────────────────────────────────────────────────────╯ +``` + +
+ +Le mode interactif offre : + +- **Sélection d'un préréglage d'architecture** (`minimal` / `single-module` / `classic-layered` / `domain-starter`) qui choisit le modèle de base et la disposition de projet adaptés +- **Sélection guidée** des bases de données, de l'authentification, des tâches en arrière-plan, de la mise en cache, de la supervision et plus encore +- **Génération automatique de code** pour les fonctionnalités sélectionnées — varie selon le préréglage (régénération de `main.py` pour `minimal` / `single-module` ; préservation du `main.py` fourni par le modèle et superposition de modules de configuration pour `classic-layered` / `domain-starter`) +- **Génération Docker adaptée au préréglage** — le `CMD` du `Dockerfile` généré pointe vers le véritable point d'entrée du préréglage (`src.main:app` ou `src.app.main:app`) +- **Gestion intelligente des dépendances** avec compatibilité automatique pour pip +- **Validation des fonctionnalités** avec des avertissements de câblage manuel pour les sélections que le préréglage ne peut pas câbler automatiquement +- **Marqueurs d'identité** dans le `pyproject.toml` généré (marqueur de description + table `[tool.fastapi-fastkit]`) afin que `is_fastkit_project()` puisse reconnaître les projets générés par la suite + +### Ajouter une nouvelle route au projet FastAPI + +`FastAPI-fastkit` facilite l'extension de votre projet FastAPI. + +Ajoutez un nouveau point d'extrémité de route à votre projet FastAPI avec : + +
+ +```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` │ +╰───────────────────────────────────────────────────────╯ +``` + +
+ +### Déployer immédiatement un projet de démonstration FastAPI structuré + +Vous pouvez aussi démarrer avec un projet de démonstration FastAPI structuré. + +Les projets de démonstration sont composés de diverses piles techniques avec des points d'extrémité CRUD d'items simples implémentés. + +Déployez immédiatement un projet de démonstration FastAPI structuré avec : + +
+ +```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~! │ +╰───────────────────────────────────────────────────────╯ +``` + +
+ +Pour consulter la liste des démos FastAPI disponibles, vérifiez avec : + +
+ +```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 │ +└────────────────────────┴───────────────────────────────────────────────────────┘ +``` + +
+ +## Documentation + +Pour des guides complets et des instructions d'utilisation détaillées, explorez notre documentation : + +- 📚 **[Guide de l'utilisateur](user-guide/quick-start.md)** — guides détaillés d'installation et d'utilisation +- 🎯 **[Tutoriel](tutorial/getting-started.md)** — tutoriels pas à pas pour les débutants +- 📖 **[Référence CLI](user-guide/cli-reference.md)** — référence complète des commandes +- 🔍 **[Assurance qualité des modèles](reference/template-quality-assurance.md)** — tests automatisés et standards de qualité + +## 🚀 Tutoriels basés sur les modèles + +Apprenez FastAPI à travers des cas pratiques grâce à nos modèles déjà prêts à l'emploi : + +### 📖 Tutoriels principaux + +- **[Construire un serveur d'API basique](tutorial/basic-api-server.md)** — créez votre premier serveur FastAPI à l'aide du modèle `fastapi-default` +- **[Construire une API CRUD asynchrone](tutorial/async-crud-api.md)** — développez une API asynchrone haute performance avec le modèle `fastapi-async-crud` +- **[Projet orienté domaine (Domain Starter)](tutorial/domain-starter.md)** — construisez une API de taille moyenne avec le modèle `fastapi-domain-starter`, l'option moderne recommandée + +### 🗄️ Base de données et infrastructure + +- **[Intégration avec une base de données](tutorial/database-integration.md)** — exploitez PostgreSQL + SQLAlchemy avec le modèle `fastapi-psql-orm` +- **[Conteneurisation Docker et déploiement](tutorial/docker-deployment.md)** — configurez un environnement de déploiement en production avec le modèle `fastapi-dockerized` + +### ⚡ Fonctionnalités avancées + +- **[Gestion des réponses personnalisées et conception d'API avancée](tutorial/custom-response-handling.md)** — construisez des API d'entreprise avec le modèle `fastapi-custom-response` +- **[Intégration avec MCP](tutorial/mcp-integration.md)** — créez un serveur d'API intégré à des modèles d'IA avec le modèle `fastapi-mcp` + +Chaque tutoriel propose : + +- ✅ **Exemples pratiques** — du code directement utilisable dans des projets réels +- ✅ **Guides pas à pas** — explications détaillées faciles à suivre pour les débutants +- ✅ **Bonnes pratiques** — motifs standards de l'industrie et considérations de sécurité +- ✅ **Méthodes d'extension** — conseils pour faire passer votre projet au niveau supérieur + +## Contribution + +Nous accueillons avec plaisir les contributions de la communauté ! FastAPI-fastkit est conçu pour aider les nouveaux venus à Python et à FastAPI, et vos contributions peuvent avoir un impact significatif. + +### Ce que vous pouvez apporter + +- 🚀 **Nouveaux modèles FastAPI** — ajoutez des modèles pour différents cas d'usage +- 🐛 **Corrections de bogues** — aidez-nous à améliorer la stabilité et la fiabilité +- 📚 **Documentation** — améliorez les guides, les exemples et les traductions +- 🧪 **Tests** — augmentez la couverture des tests et ajoutez des tests d'intégration +- 💡 **Fonctionnalités** — proposez et implémentez de nouvelles fonctionnalités CLI + +### Premiers pas pour contribuer + +Pour commencer à contribuer à FastAPI-fastkit, consultez nos guides complets : + +- **[Configuration de l'environnement de développement](contributing/development-setup.md)** — guide complet pour configurer votre environnement de développement +- **[Directives de code](contributing/code-guidelines.md)** — standards de codage et bonnes pratiques +- **[CONTRIBUTING.md](https://github.com/bnbong/FastAPI-fastkit/blob/main/CONTRIBUTING.md)** — guide de contribution complet +- **[CODE_OF_CONDUCT.md](https://github.com/bnbong/FastAPI-fastkit/blob/main/CODE_OF_CONDUCT.md)** — principes du projet et standards de la communauté +- **[SECURITY.md](https://github.com/bnbong/FastAPI-fastkit/blob/main/SECURITY.md)** — directives de sécurité et signalement + +## Pourquoi FastAPI-fastkit + +FastAPI-fastkit vise à fournir un kit de démarrage rapide et facile à utiliser pour les nouveaux utilisateurs de Python et de FastAPI. + +Cette idée est née avec l'objectif d'aider les nouveaux venus à FastAPI à apprendre dès le départ, dans la lignée de l'importance pour la production du paquet FastAPI-cli ajouté lors de la [mise à jour FastAPI 0.111.0](https://github.com/fastapi/fastapi/releases/tag/0.111.0). + +En tant qu'utilisateur passionné de FastAPI depuis longtemps, je voulais développer un projet capable de répondre à [la magnifique motivation](https://github.com/fastapi/fastapi/pull/11522#issuecomment-2264639417) exprimée par le développeur de FastAPI [tiangolo](https://github.com/tiangolo). + +FastAPI-fastkit comble l'écart entre la prise en main et la création d'applications prêtes pour la production en proposant : + +- **Productivité immédiate** pour les nouveaux venus qui pourraient être submergés par la complexité de la configuration +- **Bonnes pratiques** intégrées à chaque modèle, aidant les utilisateurs à apprendre les motifs FastAPI corrects +- **Fondations évolutives** qui accompagnent les utilisateurs depuis leurs débuts jusqu'à l'expertise +- **Modèles portés par la communauté** qui reflètent les usages réels de FastAPI + +## Étapes suivantes + +Prêt à commencer avec FastAPI-fastkit ? Suivez ces étapes : + +### 🚀 Démarrage rapide + +1. **[Installation](user-guide/installation.md)** : installer FastAPI-fastkit +2. **[Démarrage rapide](user-guide/quick-start.md)** : créer votre premier projet en 5 minutes +3. **[Tutoriel de prise en main](tutorial/getting-started.md)** : tutoriel détaillé pas à pas + +### 📚 Apprentissage avancé + +- **[Créer des projets](user-guide/creating-projects.md)** : créer des projets avec différentes piles +- **[Ajouter des routes](user-guide/adding-routes.md)** : ajouter des points d'extrémité d'API à votre projet +- **[Utiliser les modèles](user-guide/using-templates.md)** : utiliser des modèles de projet déjà prêts à l'emploi + +### 🛠️ Contribution + +Envie de contribuer à FastAPI-fastkit ? + +- **[Configuration du développement](contributing/development-setup.md)** : configurer votre environnement de développement +- **[Directives de code](contributing/code-guidelines.md)** : suivre nos standards de codage et bonnes pratiques +- **[Directives de contribution](https://github.com/bnbong/FastAPI-fastkit/blob/main/CONTRIBUTING.md)** : guide de contribution complet + +### 🔍 Référence + +- **[Référence CLI](user-guide/cli-reference.md)** : référence complète des commandes CLI +- **[Assurance qualité des modèles](reference/template-quality-assurance.md)** : tests automatisés et standards de qualité +- **[FAQ](reference/faq.md)** : foire aux questions +- **[Dépôt GitHub](https://github.com/bnbong/FastAPI-fastkit)** : code source et suivi des tickets + +## Licence + +Ce projet est sous licence MIT — consultez le fichier [LICENSE](https://github.com/bnbong/FastAPI-fastkit/blob/main/LICENSE) pour plus de détails. diff --git a/docs/fr/reference/faq.md b/docs/fr/reference/faq.md new file mode 100644 index 0000000..a50e56f --- /dev/null +++ b/docs/fr/reference/faq.md @@ -0,0 +1,784 @@ +# Foire aux questions + +Questions et réponses fréquentes sur FastAPI-fastkit. + +## Installation et configuration + +### Q. Quelles versions de Python sont prises en charge ? + +**R.** FastAPI-fastkit requiert **Python 3.12 ou supérieur**. Nous recommandons d'utiliser la dernière version stable de Python pour la meilleure expérience. + +
+ +```console +$ python --version +Python 3.12.1 + +$ pip install fastapi-fastkit +``` + +
+ +### Q. Comment installer FastAPI-fastkit ? + +**R.** Vous pouvez installer FastAPI-fastkit avec pip : + +
+ +```console +# Latest stable version +$ pip install fastapi-fastkit + +# Development version from GitHub +$ pip install git+https://github.com/bnbong/FastAPI-fastkit.git + +# Specific version +$ pip install fastapi-fastkit==1.0.0 +``` + +
+ +### Q. L'installation échoue avec des erreurs de permission + +**R.** Essayez d'installer dans un environnement virtuel ou avec les permissions utilisateur : + +
+ +```console +# Create virtual environment +$ python -m venv fastapi-env +$ source fastapi-env/bin/activate # On Windows: fastapi-env\Scripts\activate + +# Install in virtual environment +$ pip install fastapi-fastkit + +# Or install for current user only +$ pip install --user fastapi-fastkit +``` + +
+ +### Q. La commande `fastkit` est introuvable après l'installation + +**R.** Cela signifie généralement que le répertoire d'installation n'est pas dans votre PATH : + +
+ +```console +# Check if installed +$ pip show fastapi-fastkit + +# Find installation location +$ python -c "import fastapi_fastkit; print(fastapi_fastkit.__file__)" + +# Try running directly +$ python -m fastapi_fastkit --version + +# Or add to PATH (Linux/macOS) +$ export PATH="$HOME/.local/bin:$PATH" +``` + +
+ +## Création de projet + +### Q. Quelles piles de dépendances sont disponibles ? + +**R.** FastAPI-fastkit propose trois piles de dépendances : + +- **MINIMAL** : FastAPI, Uvicorn, Pydantic, Pydantic-Settings (API web basique) +- **STANDARD** : ajoute SQLAlchemy, Alembic, pytest (prise en charge des bases de données) +- **FULL** : ajoute Redis, Celery (tâches en arrière-plan) + +!!! tip "Gestionnaire de paquets par défaut" + Le gestionnaire de paquets par défaut est `uv` pour une installation plus rapide des dépendances. Vous pouvez aussi choisir `pip`, `pdm` ou `poetry`. + +
+ +```console +$ fastkit init +# Select your preferred stack during project creation +``` + +
+ +### Q. Puis-je personnaliser le modèle de projet ? + +**R.** Oui ! Vous pouvez : + +1. **Utiliser des modèles existants** avec `fastkit startdemo` +2. **Créer des modèles personnalisés** en copiant et modifiant des modèles existants +3. **Ajouter des routes au fur et à mesure** avec `fastkit addroute` + +
+ +```console +# Use pre-built templates +$ fastkit list-templates +$ fastkit startdemo + +# Add routes to existing project +$ fastkit addroute users . # Add 'users' route to current directory +$ fastkit addroute users my-project # Add 'users' route to 'my-project' +``` + +
+ +### Q. Comment créer un projet avec un format de nom particulier ? + +**R.** Les noms de projet doivent être des identifiants Python valides : + +- ✅ `my-api`, `blog_system`, `UserService` +- ❌ `my api`, `123project`, `project-name!` + +
+ +```console +$ fastkit init +Enter the project name: my_awesome_api # Valid +Enter the project name: my-awesome-api # Valid (hyphens converted to underscores) +``` + +
+ +### Q. La création de projet échoue avec « directory already exists » + +**R.** Le répertoire du projet existe déjà. Vous pouvez : + +1. **Choisir un autre nom** +2. **Supprimer le répertoire existant** (si c'est sans danger) +3. **Utiliser un autre emplacement de sortie** + +
+ +```console +# Check if directory exists +$ ls my-project + +# Remove if safe (CAUTION!) +$ rm -rf my-project + +# Or create in different location +$ mkdir projects +$ cd projects +$ fastkit init +``` + +
+ +### Q. Comment utiliser le mode interactif pour configurer un projet ? + +**R.** Utilisez `fastkit init --interactive` pour une configuration pas à pas guidée du projet avec une sélection intelligente de fonctionnalités : + +
+ +```console +$ fastkit init --interactive +``` + +
+ +Le mode interactif vous guide à travers ces étapes dans l'ordre : + +1. **Informations du projet** — nom, auteur, e-mail, description. +2. **Préréglage d'architecture** — choisit la disposition du projet. L'option recommandée par défaut est `domain-starter` ; appuyez sur Entrée pour l'accepter. Consultez la [matrice des préréglages et fonctionnalités](preset-feature-matrix.md) pour voir la disposition exacte produite par chaque préréglage et les combinaisons de fonctionnalités qui demandent un câblage manuel. +3. **Sélections de fonctionnalités** — base de données, authentification, tâches en arrière-plan, mise en cache, supervision, tests, utilitaires, déploiement. +4. **Gestionnaire de paquets et paquets personnalisés** — pip / uv / pdm / poetry, ainsi que les extras que vous souhaitez épingler. +5. **Confirmation** — un tableau récapitulatif affiche chaque choix (y compris le préréglage d'architecture) avant la création du projet. + +Le mode interactif vous permet de choisir dans un catalogue complet de fonctionnalités : + +| Catégorie | Options disponibles | +|----------|-------------------| +| **Architecture** | minimal, single-module, classic-layered, **domain-starter** (option recommandée par défaut) | +| **Base de données** | PostgreSQL, MySQL, MongoDB, Redis, SQLite | +| **Authentification** | JWT, OAuth2, FastAPI-Users, basée sur session | +| **Tâches en arrière-plan** | Celery, Dramatiq | +| **Tests** | Basic (pytest), Coverage, Advanced (avec faker, factory-boy) | +| **Mise en cache** | Redis avec fastapi-cache2 | +| **Supervision** | Loguru, OpenTelemetry, Prometheus | +| **Utilitaires** | CORS, Rate-Limiting, Pagination, WebSocket | +| **Déploiement** | Docker, docker-compose avec configurations auto-générées | + +Le mode interactif génère automatiquement : + +- `main.py` avec les fonctionnalités sélectionnées intégrées +- Des fichiers de configuration base de données et authentification lorsque les options sélectionnées prennent en charge la génération de code (par ex. PostgreSQL/MySQL/SQLite/MongoDB pour les bases de données, JWT/FastAPI-Users pour l'authentification) ; les autres options installent uniquement les paquets nécessaires +- Des fichiers de déploiement correspondant à l'option choisie (`Dockerfile` quand `Docker` est sélectionné, `docker-compose.yml` quand `docker-compose` est sélectionné) +- Une configuration de tests basée sur l'option de tests sélectionnée (les paramètres de couverture ne sont inclus que lorsque `Coverage` ou `Advanced` est sélectionné) + +### Q. Comment voir les fonctionnalités disponibles pour le mode interactif ? + +**R.** Utilisez la commande `list-features` pour afficher toutes les fonctionnalités disponibles et leurs paquets : + +
+ +```console +$ fastkit list-features +# Shows all available features organized by category +# with their associated packages +``` + +
+ +Cela vous aide à comprendre quels paquets seront installés pour chaque sélection de fonctionnalité. + +## Développement de routes + +### Q. Comment ajouter l'authentification à mes routes ? + +**R.** Créez une dépendance pour l'authentification : + +```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)): + # Verify token and return user + 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. Comment ajouter des modèles de base de données à mon projet ? + +**R.** Pour les piles STANDARD ou FULL, créez des modèles 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. Comment ajouter la validation aux données de requête ? + +**R.** Utilisez les modèles Pydantic dans vos schémas : + +```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. Comment gérer les téléversements de fichiers ? + +**R.** Utilisez `UploadFile` de FastAPI : + +```python +from fastapi import UploadFile, File + +@router.post("/upload") +async def upload_file(file: UploadFile = File(...)): + contents = await file.read() + + # Save file + with open(f"uploads/{file.filename}", "wb") as f: + f.write(contents) + + return {"filename": file.filename, "size": len(contents)} +``` + +## Modèles + +### Q. Quels modèles sont disponibles ? + +**R.** FastAPI-fastkit inclut plusieurs modèles prêts à l'emploi : + +
+ +```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. Comment utiliser un modèle spécifique ? + +**R.** Utilisez la commande `startdemo` : + +
+ +```console +$ fastkit startdemo +Enter the project name: my-blog +Select template: fastapi-psql-orm +``` + +
+ +### Q. Puis-je créer mes propres modèles ? + +**R.** Oui ! Créez une structure de répertoires et utilisez les variables de modèle : + +``` +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. Comment modifier un modèle existant ? + +**R.** Les modèles se trouvent dans le répertoire `fastapi_project_template`. Vous pouvez : + +1. **Forker le dépôt** et modifier les modèles +2. **Créer un modèle personnalisé** basé sur les existants +3. **Remplacer des fichiers précis** après la création du projet + +## Serveur de développement + +### Q. Comment démarrer le serveur de développement ? + +**R.** Utilisez la commande `runserver` depuis le répertoire de votre projet : + +
+ +```console +$ cd my-project +$ source .venv/bin/activate # Activate virtual environment +$ fastkit runserver +INFO: Uvicorn running on http://127.0.0.1:8000 +``` + +
+ +### Q. Le serveur ne démarre pas — « Address already in use » + +**R.** Le port 8000 est occupé. Utilisez un autre port ou tuez le processus existant : + +
+ +```console +# Use different port +$ fastkit runserver --port 8080 + +# Or find and kill existing process +$ lsof -ti:8000 | xargs kill -9 + +# On Windows +$ netstat -ano | findstr :8000 +$ taskkill /PID /F +``` + +
+ +### Q. Le rechargement automatique ne fonctionne pas + +**R.** Assurez-vous d'être dans le répertoire du projet et que l'environnement virtuel est activé : + +
+ +```console +# Check current directory +$ pwd +/path/to/my-project + +# Check virtual environment +$ which python +/path/to/my-project/.venv/bin/python + +# Start with explicit reload +$ fastkit runserver --reload +``` + +
+ +### Q. Comment configurer le serveur pour la production ? + +**R.** N'utilisez pas le serveur de développement en production. À la place : + +```python +# Use gunicorn or similar WSGI server +$ pip install gunicorn +$ gunicorn src.main:app -w 4 -k uvicorn.workers.UvicornWorker + +# Or use Docker with the fastapi-dockerized template +$ fastkit startdemo # Select fastapi-dockerized +$ docker build -t my-app . +$ docker run -p 8000:8000 my-app +``` + +## Performance et optimisation + +### Q. Comment améliorer les performances de l'API ? + +**R.** Plusieurs stratégies d'optimisation : + +1. **Utilisez async/await** pour les opérations d'E/S +2. **Ajoutez du cache** pour les opérations coûteuses +3. **Optimisez les requêtes de base de données** +4. **Utilisez des tâches en arrière-plan** pour les traitements lourds + +```python +# Async endpoint +@router.get("/users/{user_id}") +async def get_user(user_id: int): + user = await users_service.get_user_async(user_id) + return user + +# Background task +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. Comment ajouter du cache ? + +**R.** Utilisez Redis pour la mise en cache : + +```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))}" + + # Try to get from cache + cached = redis_client.get(cache_key) + if cached: + return json.loads(cached) + + # Execute function and cache result + 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(): + # Expensive operation + return complex_calculation() +``` + +### Q. Comment gérer beaucoup de requêtes concurrentes ? + +**R.** Utilisez une configuration de serveur appropriée : + +
+ +```console +# Development +$ fastkit runserver --workers 1 # Single worker for development + +# Production +$ gunicorn src.main:app -w 4 -k uvicorn.workers.UvicornWorker +$ uvicorn src.main:app --workers 4 --host 0.0.0.0 --port 8000 +``` + +
+ +## Tests + +### Q. Comment lancer les tests ? + +**R.** Utilisez pytest depuis le répertoire de votre projet : + +
+ +```console +$ cd my-project +$ source .venv/bin/activate +$ python -m pytest + +# With coverage +$ python -m pytest --cov=src + +# Specific test file +$ python -m pytest tests/test_users.py + +# With verbose output +$ python -m pytest -v +``` + +
+ +### Q. Comment écrire des tests d'API ? + +**R.** Utilisez le client de test de 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. Comment simuler les dépendances externes ? + +**R.** Utilisez les fixtures pytest et la simulation : + +```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): + # Test with mocked database + response = client.post("/api/v1/users/", json=user_data) + assert response.status_code == 201 +``` + +## Contribution + +### Q. Comment contribuer à FastAPI-fastkit ? + +**R.** Suivez ces étapes : + +1. **Forkez le dépôt** sur GitHub +2. **Mettez en place l'environnement de développement** +3. **Créez une branche de fonctionnalité** +4. **Apportez vos changements** avec des tests +5. **Soumettez une pull request** + +
+ +```console +$ git clone https://github.com/yourusername/FastAPI-fastkit.git +$ cd FastAPI-fastkit +$ make dev-setup # Set up development environment +$ git checkout -b feature/my-feature +# Make changes... +$ make dev-check # Format, lint, and test +$ git commit -m "feat: add new feature" +$ git push origin feature/my-feature +``` + +
+ +### Q. Que dois-je inclure dans une pull request ? + +**R.** Chaque pull request doit inclure : + +- [ ] **Description claire** des changements +- [ ] **Tests** pour les nouvelles fonctionnalités +- [ ] **Mise à jour de la documentation** si nécessaire +- [ ] **Respect des directives de code** +- [ ] **Toutes les vérifications passent** + +### Q. Comment signaler un bogue ? + +**R.** Créez une issue sur GitHub avec : + +1. **Description du bogue** et comportement attendu +2. **Étapes pour reproduire** +3. **Informations sur l'environnement** (OS, version de Python, etc.) +4. **Messages d'erreur** ou journaux +5. **Exemple minimal** si possible + +### Q. Comment proposer une nouvelle fonctionnalité ? + +**R.** Ouvrez une issue de demande de fonctionnalité avec : + +1. **Description claire** de la fonctionnalité +2. **Cas d'usage** et motivation +3. **Implémentation proposée** (optionnelle) +4. **Exemples** de fonctionnalités similaires + +## Dépannage + +### Q. Je rencontre des erreurs d'import + +**R.** Vérifiez votre chemin Python et votre environnement virtuel : + +
+ +```console +# Check virtual environment is activated +$ which python +/path/to/project/.venv/bin/python + +# Check Python path +$ python -c "import sys; print(sys.path)" + +# Reinstall in editable mode (for development) +$ pip install -e . +``` + +
+ +### Q. Problèmes de connexion à la base de données + +**R.** Pour les modèles avec base de données, assurez-vous que la base de données est en cours d'exécution : + +
+ +```console +# PostgreSQL template +$ docker-compose up -d postgres # Start database +$ alembic upgrade head # Run migrations + +# Check connection +$ docker-compose logs postgres +``` + +
+ +### Q. Fichiers de modèle introuvables + +**R.** Cela indique généralement un problème de chemin de modèle : + +
+ +```console +# Check available templates +$ fastkit list-templates + +# Check template directory +$ python -c "import fastapi_fastkit; print(fastapi_fastkit.__path__)" + +# Reinstall if templates missing +$ pip uninstall fastapi-fastkit +$ pip install fastapi-fastkit +``` + +
+ +### Q. Les hooks pre-commit échouent + +**R.** Installez et lancez les hooks : + +
+ +```console +$ pip install pre-commit +$ pre-commit install +$ pre-commit run --all-files + +# Fix formatting issues +$ black src/ tests/ +$ isort src/ tests/ +``` + +
+ +### Q. Les tests échouent en CI mais passent localement + +**R.** Causes courantes et solutions : + +1. **Différences d'environnement** : vérifiez que les versions de Python correspondent +2. **Dépendances manquantes** : assurez-vous que les dépendances de test sont installées +3. **Problèmes de chemin** : utilisez des imports absolus +4. **Problèmes de timing** : ajoutez des attentes appropriées dans les tests asynchrones + +
+ +```console +# Test with same Python version as CI +$ python3.12 -m pytest + +# Check for missing dependencies +$ pip install -r requirements-dev.txt + +# Run tests in isolated environment +$ tox +``` + +
+ +## Obtenir de l'aide + +### Q. Où puis-je obtenir de l'aide ? + +**R.** Plusieurs options pour obtenir de l'aide : + +- **GitHub Issues** : pour les bogues et les demandes de fonctionnalités +- **GitHub Discussions** : pour les questions et le soutien communautaire +- **Documentation** : guides utilisateur et tutoriels +- **Exemples de code** : consultez les modèles et tests existants + +### Q. Comment me tenir à jour ? + +**R.** Suivez les mises à jour du projet : + +- **Surveillez le dépôt** sur GitHub +- **Consultez les releases** pour les nouvelles fonctionnalités +- **Lisez le changelog** pour les changements cassants +- **Suivez les bonnes pratiques** dans la documentation + +!!! tip "Astuces de pro" + - Utilisez toujours des environnements virtuels pour les projets Python + - Maintenez votre installation de FastAPI-fastkit à jour + - Utilisez `fastkit --help` pour voir les commandes disponibles + - Consultez la documentation quand vous êtes bloqué + - N'hésitez pas à poser des questions dans GitHub Discussions diff --git a/docs/fr/reference/preset-feature-matrix.md b/docs/fr/reference/preset-feature-matrix.md new file mode 100644 index 0000000..de02801 --- /dev/null +++ b/docs/fr/reference/preset-feature-matrix.md @@ -0,0 +1,60 @@ +# Matrice des préréglages d'architecture et fonctionnalités + +L'interactif `fastkit init --interactive` demande un **préréglage d'architecture** ([issue #44](https://github.com/bnbong/FastAPI-fastkit/issues/44)) avant de collecter les sélections de fonctionnalités. Le préréglage façonne la disposition du projet généré — chaque préréglage embarque un modèle de base différent et place les fichiers de configuration générés à des emplacements différents, pour qu'ils cohabitent avec la structure existante plutôt qu'au sein d'une arborescence parallèle `src/config/`. + +Cette page sert de référence pour comprendre ce que fait chaque préréglage, où les fichiers sont écrits et quelles combinaisons de fonctionnalités demandent encore un câblage manuel. + +## Préréglage → modèle de base + +| Préréglage | Modèle de base | Description | +|---|---|---| +| `minimal` | `fastapi-empty` | L'application FastAPI minimale viable — le `main.py` d'amorçage est régénéré à partir de vos sélections de fonctionnalités. | +| `single-module` | `fastapi-single-module` | Application FastAPI monofichier — `main.py` est régénéré. | +| `classic-layered` | `fastapi-default` | Découpe en couches (`api/routes`, `crud`, `schemas`, `core`). Le `main.py` livré est préservé. | +| `domain-starter` | `fastapi-domain-starter` | Orienté domaine (`src/app/domains//`). Le `main.py` livré est préservé. **Valeur par défaut recommandée.** | + +## Emplacements des fichiers générés + +| Préréglage | Gestion de `main.py` | Cible de config base de données | Cible de config auth | +|---|---|---|---| +| `minimal` | régénéré à `src/main.py` | `src/config/database.py` | `src/config/auth.py` | +| `single-module` | régénéré à `src/main.py` | `src/config/database.py` | `src/config/auth.py` | +| `classic-layered` | préservé (livré par le modèle) | `src/core/database.py` | `src/core/auth.py` | +| `domain-starter` | préservé (livré par le modèle) | `src/app/core/database.py` | `src/app/core/auth.py` | + +## Prise en charge des fonctionnalités base de données / authentification par préréglage + +Ces fonctionnalités sont prises en charge dans **tous** les préréglages — l'installation du paquet réussit toujours ; la différence est de savoir si la superposition dynamique de `main.py` les câble également automatiquement. + +| Fonctionnalité | `minimal` / `single-module` | `classic-layered` / `domain-starter` | +|---|---|---| +| **Base de données** (PostgreSQL, MySQL, SQLite, MongoDB) | Génère le module de configuration **et** insère des appels `await init_db()` dans le `main.py` régénéré. | Génère le module de configuration au chemin du préréglage. Le `main.py` livré est **préservé**, câblez donc manuellement `get_db()` dans les routeurs. | +| **Authentification** (JWT, FastAPI-Users, OAuth2, basée sur session) | Génère le module de configuration d'authentification. JWT importe aussi `HTTPBearer` dans le `main.py` régénéré. | Génère le module de configuration d'authentification au chemin du préréglage. Aucun import n'est ajouté à `main.py` — câblez les dépendances manuellement. | +| **Tâches en arrière-plan** (Celery, Dramatiq) | Paquets installés ; aucune superposition de main.py aujourd'hui. | Idem. | +| **Mise en cache** (Redis) | Paquets installés ; aucune superposition de main.py aujourd'hui. | Idem. | +| **CORS** (utilitaire) | `CORSMiddleware` est ajouté au `main.py` régénéré avec `allow_origins=['*']`. | **Déjà câblé** dans le `main.py` livré (conditionné par `settings.all_cors_origins`). Activez-le en définissant `BACKEND_CORS_ORIGINS` dans `.env` — aucune modification de code nécessaire. | +| **Tests** (Basic / Coverage / Advanced) | `pytest.ini` est généré à la racine du projet. | Idem. | +| **Déploiement** (Docker, docker-compose) | `Dockerfile` et/ou `docker-compose.yml` écrits à la racine du projet. | Idem. | + +## Quand un avertissement « Préréglage compatibilité » apparaît + +Pour les préréglages qui **préservent le `main.py` livré** (`classic-layered`, `domain-starter`), certaines sélections de fonctionnalités ne seront pas câblées automatiquement dans l'application. Le CLI affiche un avertissement ponctuel à la fin de la génération listant les sélections qui nécessitent un câblage manuel : + +| Fonctionnalité sélectionnée | Déclenche un avertissement sous `classic-layered` / `domain-starter` ? | +|---|---| +| `CORS` (utilitaire) | ❌ — déjà câblé dans le `main.py` livré. Renseignez simplement `BACKEND_CORS_ORIGINS` dans `.env`. | +| `Rate-Limiting` (utilitaire) | ✅ — la configuration du limiteur `slowapi` n'est pas ajoutée | +| `Prometheus` (supervision) | ✅ — `Instrumentator().instrument(app)` n'est pas appelé | +| Toute sélection base de données / authentification | ⚠️ — les fichiers de configuration sont générés, mais vous devez les passer en `Depends()` dans vos routeurs | + +Pour les préréglages `minimal` et `single-module`, la superposition dynamique de `main.py` gère automatiquement CORS, la limitation de débit et l'instrumentation Prometheus ; aucun avertissement n'est émis. + +## Combinaisons non prises en charge (rester prudent) + +Le stratège n'essaie **délibérément pas** d'injecter du code généré dans un `main.py` livré par le modèle. Le faire risquerait de produire des imports cassés ou des routeurs dupliqués. Le contrat est : + +- Les paquets sélectionnés sont toujours installés (afin que `pip freeze` corresponde à l'intention de l'utilisateur). +- Les modules de configuration générés atterrissent toujours au chemin approprié au préréglage. +- Pour les préréglages qui préservent main, l'utilisateur est informé des sélections qui nécessitent encore un câblage manuel, plutôt que de recevoir silencieusement du code cassé. + +Si vous avez besoin du câblage automatique complet de toutes les fonctionnalités, choisissez `minimal` ou `single-module` — ils régénèrent `main.py` à partir des drapeaux de fonctionnalités. diff --git a/docs/fr/reference/template-quality-assurance.md b/docs/fr/reference/template-quality-assurance.md new file mode 100644 index 0000000..87b4050 --- /dev/null +++ b/docs/fr/reference/template-quality-assurance.md @@ -0,0 +1,218 @@ +# Assurance qualité des modèles + +FastAPI-fastkit fournit une validation automatisée et complète des modèles pour s'assurer qu'ils maintiennent une qualité élevée et restent fonctionnels dans différents environnements et avec différents gestionnaires de paquets. + +## Assurance qualité multi-couches + +FastAPI-fastkit s'appuie sur **deux systèmes d'assurance qualité complémentaires** : + +### 1. Inspection statique des modèles +**Validation automatisée hebdomadaire de la structure et de la syntaxe des modèles** + +### 2. Tests dynamiques des modèles +**Tests complets de bout en bout avec création de projets réels** + +## Inspection hebdomadaire automatisée + +Chaque mercredi à minuit (UTC), notre workflow GitHub Actions inspecte automatiquement tous les modèles FastAPI pour s'assurer qu'ils respectent les standards de qualité : + +- ✅ **Validation de la structure des fichiers** — vérifie que tous les fichiers et répertoires requis sont présents +- ✅ **Vérification des extensions de fichiers** — valide que les fichiers de modèle utilisent les bonnes extensions `.py-tpl` +- ✅ **Vérification des dépendances** — confirme que FastAPI et les dépendances requises sont correctement définies +- ✅ **Implémentation FastAPI** — vérifie que les modèles contiennent une initialisation correcte de l'application FastAPI +- ✅ **Exécution des tests** — exécute les tests du modèle pour s'assurer du bon fonctionnement + +## Système de tests automatisés des modèles + +FastAPI-fastkit inclut un **système de tests automatisés révolutionnaire** qui fournit une validation complète de chaque modèle : + +### Découverte dynamique des modèles + +Le système de tests **découvre automatiquement tous les modèles** sans configuration manuelle : + +```console +# Test all templates automatically +$ pytest tests/test_templates/test_all_templates.py -v + +# Results show all discovered templates +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] +``` + +### Couverture de tests complète + +Chaque modèle subit des **tests complets de bout en bout** : + +#### ✅ Processus de création de projet +- Copie du modèle et transformation des fichiers +- Injection des métadonnées du projet (nom, auteur, description) +- Validation de la structure des fichiers + +#### ✅ Compatibilité avec les gestionnaires de paquets +- **UV** (par défaut) : gestionnaire de paquets rapide basé sur Rust +- **PDM** : gestion moderne des dépendances Python +- **Poetry** : gestion des dépendances établie +- **PIP** : gestionnaire de paquets Python traditionnel + +#### ✅ Gestion de l'environnement virtuel +- Création de l'environnement pour chaque gestionnaire de paquets +- Vérification de l'installation des dépendances +- Flux de travail propres à chaque gestionnaire de paquets + +#### ✅ Résolution des dépendances +- Génération de `pyproject.toml` (UV, PDM, Poetry) +- Génération de `requirements.txt` (PIP) +- Conformité aux métadonnées (PEP 621) +- Configuration du système de build + +#### ✅ Validation de la structure du projet +- Identification du projet FastAPI +- Existence des fichiers requis +- Vérification de la structure des répertoires + +### Exemples d'exécution des tests + +**Lancer tous les tests des modèles :** +```console +$ pytest tests/test_templates/test_all_templates.py -v +``` + +**Tester un modèle précis :** +```console +$ pytest tests/test_templates/test_all_templates.py::TestAllTemplates::test_template_creation[fastapi-default] -v +``` + +**Tester avec un environnement PDM :** +```console +$ pdm run pytest tests/test_templates/test_all_templates.py -v +``` + +### Intégration continue + +Le système de tests automatisés s'exécute dans les **pipelines CI/CD** : + +- ✅ **Validation des pull requests** : chaque PR teste les modèles concernés +- ✅ **Tests nocturnes** : validation complète de la suite de modèles +- ✅ **Tests des gestionnaires de paquets** : validation croisée avec tous les gestionnaires +- ✅ **Tests d'environnement** : plusieurs versions de Python et plateformes + +### Avantages pour les contributeurs + +**Tests sans configuration :** + +- 🚀 Ajout d'un nouveau modèle → tests automatiques +- ⚡ Aucune création manuelle de fichier de test requise +- 🛡️ Standards de qualité cohérents + +**Couverture complète :** + +- 🔍 Tests de bout en bout de la création de projet +- 📦 Validation multi-gestionnaire de paquets +- 🏗️ Tests complets de résolution des dépendances +- ✅ Simulation d'usage réel + +**Expérience développeur :** + +- 🎯 **Concentrez-vous sur le contenu du modèle** : les tests sont automatiques +- 🔄 **Retour immédiat** : exécution rapide des tests +- 📊 **Résultats clairs** : rapport de tests détaillé +- 🚫 **Pas de plomberie** : zéro configuration de test nécessaire + +## Inspection manuelle des modèles + +Pour le développement et le débogage, vous pouvez inspecter manuellement les modèles avec notre script d'inspection local ou les commandes Makefile : + +### Utiliser le script d'inspection directement + +```console +# Inspect all templates +$ python scripts/inspect-templates.py + +# Inspect specific templates +$ python scripts/inspect-templates.py --templates fastapi-default,fastapi-async-crud + +# Verbose output with detailed information +$ python scripts/inspect-templates.py --verbose + +# Save results to custom file +$ python scripts/inspect-templates.py --output my_results.json +``` + +### Utiliser les commandes Makefile + +```console +# Inspect all templates +$ make inspect-templates + +# Inspect with verbose output +$ make inspect-templates-verbose + +# Inspect specific templates +$ make inspect-template TEMPLATES="fastapi-default,fastapi-async-crud" +``` + +## Résultats de l'inspection + +- Les **inspections réussies** sont consignées dans les sorties et artefacts du workflow +- Les **inspections échouées** créent automatiquement des issues GitHub avec des rapports d'erreur détaillés +- L'**historique d'inspection** est conservé pendant 30 jours dans les artefacts GitHub Actions + +## Comprendre la sortie d'inspection + +Quand vous exécutez l'inspection de modèles, vous verrez une sortie comme celle-ci : + +```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 +``` + +## Exigences des modèles + +Pour qu'un modèle passe l'inspection, il doit remplir ces exigences : + +### Structure des fichiers +- Doit contenir un répertoire `src/` avec des fichiers source Python +- Les fichiers Python doivent utiliser l'extension `.py-tpl` +- Doit inclure un répertoire `tests/` et un fichier `README.md-tpl` +- Doit inclure **au moins un** fichier de métadonnées : + - `pyproject.toml-tpl` (préféré, PEP 621), ou + - `setup.py-tpl` (legacy, toujours accepté) +- `requirements.txt-tpl` est optionnel quand `pyproject.toml-tpl` déclare `[project].dependencies` + +### Exigences FastAPI +- Doit contenir l'initialisation de l'application FastAPI +- Doit déclarer `fastapi` comme dépendance dans au moins l'un de : `pyproject.toml-tpl` `[project].dependencies`, `requirements.txt-tpl`, ou `setup.py-tpl` `install_requires` +- Doit avoir une syntaxe Python valide dans tous les fichiers de modèle + +### Marqueurs d'identité +Les modèles doivent porter des marqueurs d'identité FastAPI-fastkit afin que les projets générés se distinguent de projets FastAPI non liés dans l'espace de travail de l'utilisateur : + +- `pyproject.toml-tpl` — à la fois un préfixe `[FastAPI-fastkit templated]` dans `description` et une table `[tool.fastapi-fastkit]` avec `managed = true`. +- `setup.py-tpl` — préfixe `[FastAPI-fastkit templated]` dans l'argument `description` passé à `setup()`. + +`is_fastkit_project()` accepte n'importe lequel des deux (pyproject prend la priorité, setup.py est le repli legacy ; la comparaison est insensible à la casse). L'injection de métadonnées garantit que les marqueurs finissent dans les projets générés même si un modèle les oublie. + +### Standards de qualité +- Tous les fichiers de modèle doivent être syntaxiquement corrects +- Les dépendances doivent être correctement spécifiées +- La structure du modèle doit suivre les conventions FastAPI-fastkit + +Cette assurance qualité automatisée garantit que tous les modèles restent fiables et prêts pour un usage en production. diff --git a/docs/fr/reference/translation-status.md b/docs/fr/reference/translation-status.md new file mode 100644 index 0000000..716daac --- /dev/null +++ b/docs/fr/reference/translation-status.md @@ -0,0 +1,82 @@ +# État des traductions + +FastAPI-fastkit publie sa documentation dans plusieurs langues, mais ces traductions **n'avancent pas toutes au même rythme**. Cette page est la référence principale pour savoir ce qui est réellement traduit, ce qui s'affiche lorsqu'une page ne l'est pas encore et comment contribuer. + +## Source de vérité + +> **L'anglais (`en`) est la référence principale.** Tous les comportements du produit, du CLI et de l'API décrits dans la documentation sont d'abord rédigés dans les fichiers anglais. Les autres locales sont des traductions de cette base anglaise et peuvent prendre du retard par rapport à une version. +> +> Si une page traduite contredit la page anglaise, **faites confiance à la page anglaise** jusqu'à ce que la traduction soit mise à jour. + +Les fichiers anglais se trouvent sous [`docs/en/`](https://github.com/bnbong/FastAPI-fastkit/tree/main/docs/en). Toutes les autres locales (`docs/ko/`, `docs/ja/`, …) sont des cibles de traduction. + +Le `CHANGELOG.md` à la racine du dépôt fait aussi partie de cette référence anglaise. Des pages locales `changelog.md` peuvent exister comme pages d'accès, mais elles réutilisent volontairement l'historique de versions canonique en anglais au lieu d'en maintenir des copies traduites. + +## Complétude par locale + +Les nombres ci-dessous comptent les pages Markdown dans l'arborescence de chaque locale par rapport à la source anglaise. Ils reflètent ce qui est réellement présent dans le dépôt — pas ce qui apparaît dans le sélecteur de langue (la section suivante l'explique). + +| Locale | État | Pages Markdown | Notes | +|---|---|---:|---| +| 🇬🇧 Anglais (`en`) | ✅ Source de vérité | 26 / 26 | Référence. | +| 🇰🇷 Coréen (`ko`) | ✅ Complet | 26 / 26 | Toutes les pages de la locale sont présentes. Phase 1 : top-level + cœur du guide de l'utilisateur ; Phase 2 : reste du guide de l'utilisateur + tous les tutoriels ; Phase 3 : pages de contribution + de référence. `docs/ko/changelog.md` réutilise intentionnellement le `CHANGELOG.md` canonique en anglais. | +| 🇯🇵 Japonais (`ja`) | ✅ Complet | 26 / 26 | Toutes les pages de la locale sont présentes. Phase 1 : top-level + cœur du guide de l'utilisateur ; Phase 2 : reste du guide de l'utilisateur + tous les tutoriels ; Phase 3 : pages de contribution + de référence. `docs/ja/changelog.md` réutilise intentionnellement le `CHANGELOG.md` canonique en anglais. | +| 🇨🇳 Chinois (`zh`) | 🔴 Squelette | 0 / 26 | Cible de build uniquement. Toutes les pages retombent sur l'anglais. | +| 🇪🇸 Espagnol (`es`) | ✅ Complet | 26 / 26 | Toutes les pages de la locale sont présentes. Phase 1 : top-level + cœur du guide de l'utilisateur ; Phase 2 : reste du guide de l'utilisateur + tous les tutoriels ; Phase 3 : pages de contribution + de référence. `docs/es/changelog.md` réutilise intentionnellement le `CHANGELOG.md` canonique en anglais. | +| 🇫🇷 Français (`fr`) | ✅ Complet | 26 / 26 | Toutes les pages de la locale sont présentes. Phase 1 : top-level + cœur du guide de l'utilisateur ; Phase 2 : reste du guide de l'utilisateur + tous les tutoriels ; Phase 3 : pages de contribution + de référence. `docs/fr/changelog.md` réutilise intentionnellement le `CHANGELOG.md` canonique en anglais. | +| 🇩🇪 Allemand (`de`) | 🔴 Squelette | 0 / 26 | Cible de build uniquement. Toutes les pages retombent sur l'anglais. | + +*Instantané vérifié le 2026-05-15 ; la ligne `fr` a été recomptée sur la branche actuelle après la fin de la Phase 3 (pages de contribution + de référence). Le français comporte désormais toutes les pages de la locale et `docs/fr/changelog.md` pointe vers le changelog canonique en anglais.* Ces compteurs sont maintenus à la main ; pour recompter l'état actuel depuis la racine du dépôt, exécutez : + +```console +$ for loc in en ko ja zh es fr de; do + echo "$loc: $(find docs/$loc -name '*.md' 2>/dev/null | wc -l | tr -d ' ')" + done +``` + +Si le recomptage ne correspond pas au tableau, c'est que le tableau est obsolète — mettez-le à jour (ou ouvrez une PR / un ticket signalant l'écart). + +Légende : + +- ✅ **Langue source** — la locale dans laquelle on écrit en premier. +- 🟡 **Partiel** — certaines pages sont traduites ; les pages manquantes retombent sur l'anglais. +- 🔴 **Squelette** — l'entrée du sélecteur de langue existe, mais aucune page traduite n'est encore livrée. Le site rend le contenu anglais sous les libellés de navigation traduits. + +## Comment fonctionne le repli + +Le site de documentation utilise [`mkdocs-static-i18n`](https://github.com/ultrabug/mkdocs-static-i18n) avec `fallback_to_default: true`. Cela signifie : + +- Pour chaque locale traduite, MkDocs n'écrit que les pages présentes dans le répertoire de cette locale. +- Pour chaque page **absente** d'une locale, le build retombe sur la version anglaise de cette page. +- Le sélecteur de langue global liste toujours toutes les locales configurées, peu importe le nombre de pages que chacune contient, car le build produit une URL accessible pour chaque cas (page traduite ou, à défaut, repli anglais). + +Ainsi, une entrée 🔴 Squelette dans le sélecteur de langue **ne signifie pas** que la documentation est traduite — seulement que la cible de build de la locale est configurée. Ce comportement est intentionnel (les contributeurs externes peuvent traduire une page à la fois sans casser le maillage de liens), mais il donne l'impression que le sélecteur de langue est plus complet que ne l'est réellement le contenu sous-jacent. + +## Comment lire le site + +- **Par défaut, restez en anglais** si vous voulez l'information la plus précise et la plus à jour. +- **Utilisez une locale traduite** uniquement après avoir consulté l'état de cette locale sur cette page. Si l'état est 🟡 ou 🔴 et que vous tombez sur un sujet non traduit, vous lisez en réalité un repli anglais sous un libellé de navigation traduit. + +## Comment aider + +L'organisation actuelle repose sur **un ticket de suivi par locale**, avec un travail découpé en **phases**. Par exemple, `ko` a été traité en Phase 1 (pages principales + cœur du guide utilisateur), en Phase 2 (reste du guide utilisateur + tous les tutoriels) puis en Phase 3 (pages de contribution + de référence). Chaque phase arrive dans sa propre PR, afin que les relecteurs puissent valider un ensemble cohérent sans attendre que toute la locale soit terminée. + +Si vous souhaitez contribuer : + +1. Lisez le [Guide de traduction](../contributing/translation-guide.md) pour le flux de travail, l'outillage et les conventions de style. +2. **Vérifiez ou ouvrez d'abord le ticket de suivi de la locale.** Si une locale a déjà un ticket de suivi ouvert, réservez-y une phase (ou une page précise d'une phase) pour éviter les doublons. S'il n'existe pas de ticket de suivi pour la locale qui vous intéresse, ouvrez-en un qui liste les pages appartenant à chaque phase, puis commencez par la Phase 1. +3. **L'idéal est une PR par phase.** Des PR plus petites du type « corriger cette seule page » restent les bienvenues, surtout pour corriger une traduction désynchronisée. Mais pour lancer une nouvelle locale, regrouper le travail par phase aide à garder un glossaire et des liens formulés de manière cohérente au sein du même ensemble. +4. Ouvrez la PR en ajoutant les fichiers sous `docs//`. Conservez des noms de fichiers identiques à la source anglaise pour que MkDocs les détecte automatiquement. +5. Traitez les pages de changelog localisées comme des pages d'accès vers le `CHANGELOG.md` canonique en anglais, sauf si la politique du projet change explicitement. +6. Mettez à jour le tableau de cette page pour refléter le nouveau niveau de couverture (utilisez l'extrait de recomptage en haut de cette page) et mettez à jour la date « Instantané vérifié » pour que les relecteurs sachent quand la vérification a été faite pour la dernière fois. Indiquez dans la colonne « Notes » quelle phase est terminée si la locale est encore partielle. + +Les signalements de bogues concernant des pages traduites désynchronisées par rapport à la source anglaise sont les bienvenus — merci de relier la page anglaise et la page traduite pour faciliter le triage. + +## Pourquoi publier malgré tout des locales 🔴 Squelette + +Deux raisons : + +1. **Espace d'URL prévisible.** Chaque locale a déjà son sous-arbre `//` accessible, de sorte que lorsqu'une page traduite arrive, le lien est stable dès le premier jour — y compris les liens publiés dans ce guide. +2. **Moins de friction pour les contributeurs.** Les personnes qui traduisent une seule page n'ont pas besoin, en plus, de configurer une nouvelle cible de build dans MkDocs : il leur suffit d'ajouter le fichier. + +Si une locale reste en 🔴 Squelette sans activité de contribution pendant une période prolongée, nous pourrons reconsidérer le maintien de sa cible de build. Cette décision est suivie séparément et **n'est pas** quelque chose que cette page de statut modifie silencieusement. diff --git a/docs/fr/tutorial/async-crud-api.md b/docs/fr/tutorial/async-crud-api.md new file mode 100644 index 0000000..c755dae --- /dev/null +++ b/docs/fr/tutorial/async-crud-api.md @@ -0,0 +1,665 @@ +# Construire des API CRUD asynchrones + +Apprenez à construire des API CRUD haute performance en exploitant les capacités de traitement asynchrone de FastAPI. Dans ce tutoriel, nous allons implémenter des E/S fichier asynchrones et un traitement de données efficace en utilisant le modèle `fastapi-async-crud`. + +## Ce que vous apprendrez dans ce tutoriel + +- Comprendre les applications FastAPI asynchrones +- Opérations CRUD asynchrones avec la syntaxe `async/await` +- Traitement de fichiers asynchrone avec aiofiles +- Écrire et exécuter des tests asynchrones +- Techniques d'optimisation des performances + +## Prérequis + +- Tutoriel [Serveur d'API basique](basic-api-server.md) terminé +- Compréhension des concepts de base de `async/await` en Python +- FastAPI-fastkit installé + +## Pourquoi le traitement asynchrone est nécessaire + +Comprenons la différence entre le traitement synchrone et asynchrone : + +### Traitement synchrone + +```python +def process_items(): + item1 = read_file("item1.json") # Wait 2 seconds + item2 = read_file("item2.json") # Wait 2 seconds + item3 = read_file("item3.json") # Wait 2 seconds + return [item1, item2, item3] # Total: 6 seconds +``` + +### Traitement asynchrone + +```python +async def process_items(): + item1_task = read_file_async("item1.json") # Start concurrently + item2_task = read_file_async("item2.json") # Start concurrently + item3_task = read_file_async("item3.json") # Start concurrently + + items = await asyncio.gather(item1_task, item2_task, item3_task) + return items # Total: 2 seconds +``` + +## Étape 1 : Créer un projet CRUD asynchrone + +Créez un projet avec le modèle `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! +``` + +
+ +## Étape 2 : Analyser la structure du projet + +Examinons les différences clés dans le projet généré : + +``` +async-todo-api/ +├── src/ +│ ├── main.py # Asynchronous FastAPI application +│ ├── api/ +│ │ └── routes/ +│ │ └── items.py # Asynchronous CRUD endpoints +│ ├── crud/ +│ │ └── items.py # Asynchronous data processing logic +│ ├── schemas/ +│ │ └── items.py # Data models (same) +│ ├── mocks/ +│ │ └── mock_items.json # JSON file database +│ └── core/ +│ └── config.py # Configuration file +└── tests/ + ├── conftest.py # Asynchronous test configuration + └── test_items.py # Asynchronous test cases +``` + +### Différences clés + +1. **aiofiles** : E/S fichier asynchrones +2. **pytest-asyncio** : prise en charge des tests asynchrones +3. **motif async/await** : toutes les opérations CRUD implémentées de manière asynchrone + +## Étape 3 : Comprendre la logique CRUD asynchrone + +### Traitement asynchrone des données (`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]: + """Asynchronously read data from JSON file""" + 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: + """Asynchronously write data to JSON file""" + 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]: + """Retrieve all items (asynchronous)""" + data = await self._read_data() + return [Item(**item) for item in data] + + async def get_item(self, item_id: int) -> Optional[Item]: + """Retrieve specific item (asynchronous)""" + 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: + """Create new item (asynchronous)""" + 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]: + """Update item (asynchronous)""" + 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: + """Delete item (asynchronous)""" + 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 +``` + +### Points d'extrémité d'API asynchrones (`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(): + """Retrieve all items (asynchronous)""" + return await crud.get_items() + +@router.get("/{item_id}", response_model=Item) +async def read_item(item_id: int): + """Retrieve specific item (asynchronous)""" + 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): + """Create new item (asynchronous)""" + return await crud.create_item(item) + +@router.put("/{item_id}", response_model=Item) +async def update_item(item_id: int, item_update: ItemUpdate): + """Update item (asynchronous)""" + 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): + """Delete item (asynchronous)""" + 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" + ) +``` + +## Étape 4 : Lancer le serveur et tester + +Allez dans le répertoire du projet et lancez le serveur : + +
+ +```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. +``` + +
+ +### Tests de performance + +Vérifions les performances du traitement asynchrone. Essayez d'envoyer plusieurs requêtes simultanément : + +**Test de requêtes concurrentes (script 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) # Create 10 items concurrently + ] + + 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)}") + +# Run test +# asyncio.run(test_concurrent_requests()) +``` + +## Étape 5 : Écrire des tests asynchrones + +### Configuration des tests (`tests/conftest.py`) + +```python +import pytest +import asyncio +from httpx import AsyncClient +from src.main import app + +@pytest.fixture(scope="session") +def event_loop(): + """Event loop configuration""" + loop = asyncio.get_event_loop_policy().new_event_loop() + yield loop + loop.close() + +@pytest.fixture +async def async_client(): + """Asynchronous test client""" + async with AsyncClient(app=app, base_url="http://test") as client: + yield client +``` + +### Cas de tests asynchrones (`tests/test_items.py`) + +```python +import pytest +from httpx import AsyncClient + +@pytest.mark.asyncio +async def test_create_item_async(async_client: AsyncClient): + """Asynchronous item creation test""" + 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): + """Asynchronous item list retrieval test""" + 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): + """Concurrent operations test""" + import asyncio + + # Create multiple items concurrently + 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) + + # Verify all requests succeeded + for response in responses: + assert response.status_code == 201 + + # Verify created items + response = await async_client.get("/items/") + items = response.json() + assert len(items) >= 5 +``` + +### Lancer les tests + +
+ +```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 ======================== +``` + +
+ +## Étape 6 : Surveillance et optimisation des performances + +### Ajouter un middleware de mesure du temps de réponse + +Ajoutons une surveillance des performances à `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): + """Add request processing time to headers""" + 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!"} +``` + +### Implémenter un traitement par lots asynchrone + +Ajoutons des points d'extrémité par lots pour traiter plusieurs items à la fois : + +```python +# À ajouter dans `src/api/routes/items.py` + +@router.post("/batch", response_model=List[Item]) +async def create_items_batch(items: List[ItemCreate]): + """Créer plusieurs items en parallèle (traitement par lots)""" + import asyncio + + # Execute all item creation tasks concurrently + 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): + """Retrieve multiple items concurrently (batch processing)""" + import asyncio + + # Parse comma-separated IDs + ids = [int(id.strip()) for id in item_ids.split(",")] + + # Execute all item retrieval tasks concurrently + tasks = [crud.get_item(item_id) for item_id in ids] + items = await asyncio.gather(*tasks) + + # Return only non-None items + return [item for item in items if item is not None] +``` + +### Tester le traitement par lots + +
+ +```console +# Batch creation test +$ 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} + ]' + +# Batch retrieval test +$ curl -X GET "http://127.0.0.1:8000/items/batch/1,2,3" +``` + +
+ +## Étape 7 : Motifs asynchrones avancés + +### Implémenter la limitation de débit + +```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) + + # remove old request records + self.requests[client_ip] = [ + req_time for req_time in self.requests[client_ip] + if req_time > cutoff + ] + + # check current request count + if len(self.requests[client_ip]) >= self.max_requests: + return False + + # add current request record + self.requests[client_ip].append(now) + return True + +# global rate limiter instance +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 +``` + +### Implémenter une mise en cache asynchrone + +```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]: + # remove expired items + 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) + +# global cache instance +cache = AsyncCache() + +# modify CRUD methods to use cache +async def get_items_cached(self) -> List[Item]: + """Retrieve items using cache""" + cache_key = "all_items" + cached_items = await cache.get(cache_key) + + if cached_items: + return cached_items + + # if cache is not found, read from file + items = await self.get_items() + await cache.set(cache_key, items, ttl_seconds=60) # 1 minute cache + + return items +``` + +## Étape 8 : Considérations pour la production + +### Gérer les pools de connexions + +```python +# add to src/core/config.py +class Settings(BaseSettings): + # ... existing settings ... + + # asynchronous processing related settings + MAX_CONCURRENT_REQUESTS: int = 100 + REQUEST_TIMEOUT: int = 30 + CONNECTION_POOL_SIZE: int = 20 + +settings = Settings() +``` + +### Améliorer la gestion des erreurs + +```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]: + """Execute safe asynchronous operation""" + 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") + +# usage example +@router.get("/safe/{item_id}") +async def read_item_safe(item_id: int): + return await safe_async_operation(crud.get_item, item_id) +``` + +## Étapes suivantes + +Vous avez terminé la construction d'une API CRUD asynchrone ! Étapes suivantes à essayer : + +1. **[Intégration de base de données](database-integration.md)** — utiliser PostgreSQL avec SQLAlchemy asynchrone +2. **[Conteneurisation Docker](docker-deployment.md)** — conteneuriser des applications asynchrones +3. **[Gestion des réponses personnalisées](custom-response-handling.md)** — formats de réponse avancés et gestion des erreurs + + + +## Résumé + +Dans ce tutoriel, nous avons utilisé FastAPI asynchrone pour : + +- ✅ Implémenter des opérations CRUD asynchrones +- ✅ Optimiser les E/S fichier avec aiofiles +- ✅ Gérer des requêtes concurrentes et tester les performances +- ✅ Écrire et exécuter des tests asynchrones +- ✅ Implémenter le traitement par lots et des motifs asynchrones avancés +- ✅ Répondre aux considérations de production (mise en cache, gestion des erreurs, gestion des connexions) + +Maîtriser le traitement asynchrone vous permet de construire des serveurs d'API haute performance ! diff --git a/docs/fr/tutorial/basic-api-server.md b/docs/fr/tutorial/basic-api-server.md new file mode 100644 index 0000000..f1ce397 --- /dev/null +++ b/docs/fr/tutorial/basic-api-server.md @@ -0,0 +1,398 @@ +# Construire un serveur d'API basique + +Apprenez à construire rapidement un serveur d'API REST simple avec FastAPI-fastkit. Ce tutoriel s'adresse aux débutants en FastAPI et couvre la création d'API CRUD basiques. + +## Ce que vous apprendrez dans ce tutoriel + +- Créer un serveur d'API basique avec la commande `fastkit startdemo` +- Comprendre la structure d'un projet FastAPI +- Utiliser des points d'extrémité CRUD basiques +- Tester et documenter l'API +- Méthodes d'extension du projet + +## Prérequis + +- Python 3.12 ou supérieur installé +- FastAPI-fastkit installé (`pip install fastapi-fastkit`) +- Connaissances de base de Python + +## Étape 1 : Créer un projet d'API basique + +Créons un serveur d'API basique avec le modèle `fastapi-default`. + +
+ +```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! +``` + +
+ +## Étape 2 : Comprendre la structure du projet généré + +Examinons la structure du projet généré : + +``` +my-first-api/ +├── README.md # Documentation du projet +├── requirements.txt # Liste des dépendances +├── setup.py # Configuration du package +├── scripts/ +│ └── run-server.sh # Script de démarrage du serveur +├── src/ # Code source principal +│ ├── main.py # Point d'entrée de l'application FastAPI +│ ├── core/ +│ │ └── config.py # Gestion de la configuration +│ ├── api/ +│ │ ├── api.py # Regroupement des routeurs API +│ │ └── routes/ +│ │ └── items.py # Points d'extrémité liés aux items +│ ├── schemas/ +│ │ └── items.py # Définitions des modèles de données +│ ├── crud/ +│ │ └── items.py # Logique de traitement des données +│ └── mocks/ +│ └── mock_items.json # Données de test +└── tests/ # Code de test + ├── __init__.py + ├── conftest.py + └── test_items.py +``` + +### Description des fichiers clés + +- **`src/main.py`** : point d'entrée de l'application FastAPI +- **`src/api/routes/items.py`** : définitions des points d'extrémité d'API relatifs aux items +- **`src/schemas/items.py`** : définitions des structures de données de requête / réponse +- **`src/crud/items.py`** : logique des opérations sur les données +- **`src/mocks/mock_items.json`** : données d'exemple pour le développement + +## Étape 3 : Lancer le serveur + +Déplaçons-nous dans le répertoire du projet généré et lançons le serveur. + +
+ +```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. +``` + +
+ +Une fois le serveur lancé avec succès, vous pouvez accéder aux URL suivantes dans votre navigateur : + +- **Serveur d'API** : http://127.0.0.1:8000 +- **Documentation Swagger UI** : http://127.0.0.1:8000/docs +- **Documentation ReDoc** : http://127.0.0.1:8000/redoc + +## Étape 4 : Explorer les points d'extrémité de l'API + +L'API générée fournit par défaut les points d'extrémité suivants : + +| Méthode | Point d'extrémité | Description | +|--------|----------|-------------| +| GET | `/items/` | Récupérer tous les items | +| GET | `/items/{item_id}` | Récupérer un item précis | +| POST | `/items/` | Créer un nouvel item | +| PUT | `/items/{item_id}` | Mettre à jour un item | +| DELETE | `/items/{item_id}` | Supprimer un item | + +### Tester l'API + +**1. Récupérer tous les 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. Créer un nouvel 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. Récupérer un item précis** + +
+ +```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 +} +``` + +
+ +## Étape 5 : Tester l'API avec Swagger UI + +Rendez-vous sur http://127.0.0.1:8000/docs dans votre navigateur pour voir la documentation d'API générée automatiquement. + +Ce que vous pouvez faire avec Swagger UI : + +1. **Voir les points d'extrémité d'API** : visualiser tous les points d'extrémité disponibles +2. **Consulter les schémas de requête / réponse** : voir les formats d'entrée / sortie pour chaque point d'extrémité +3. **Tester les API directement** : effectuer de vrais appels d'API avec le bouton « Try it out » +4. **Voir les données d'exemple** : consulter les données d'exemple de requête / réponse pour chaque point d'extrémité + +### Comment utiliser Swagger UI + +1. Cliquez sur le point d'extrémité GET `/items/` +2. Cliquez sur le bouton « Try it out » +3. Cliquez sur le bouton « Execute » +4. Consultez la réponse du serveur + +## Étape 6 : Comprendre la structure du code + +### Application principale (`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"} +``` + +### Schéma d'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 +``` + +### Logique 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) +``` + +## Étape 7 : Étendre le projet + +### Ajouter de nouvelles routes + +Vous pouvez ajouter de nouveaux points d'extrémité avec la commande `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! +``` + +
+ +Cette commande crée les fichiers suivants : + +- `src/api/routes/user.py` — points d'extrémité relatifs aux utilisateurs +- `src/schemas/user.py` — modèles de données utilisateur +- `src/crud/user.py` — logique de traitement des données utilisateur + +### Personnaliser la configuration d'environnement + +Vous pouvez modifier le fichier `src/core/config.py` pour changer les paramètres du projet : + +```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() +``` + +## Étape 8 : Lancer les tests + +Le projet inclut des tests basiques : + +
+ +```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 ======================== +``` + +
+ +## Étapes suivantes + +Vous avez terminé la construction d'un serveur d'API basique ! Étapes suivantes à essayer : + +1. **[Construire des API CRUD asynchrones](async-crud-api.md)** — apprendre un traitement asynchrone plus complexe +2. **[Intégration de base de données](database-integration.md)** — utiliser PostgreSQL et SQLAlchemy +3. **[Conteneurisation Docker](docker-deployment.md)** — préparer un déploiement en production +4. **[Gestion des réponses personnalisées](custom-response-handling.md)** — configuration avancée des formats de réponse + +## Dépannage + +### Problèmes courants + +**Q. Le serveur ne démarre pas** +R. Vérifiez que votre environnement virtuel est activé et que les dépendances sont correctement installées. + +**Q. Impossible d'accéder aux points d'extrémité de l'API** +R. Vérifiez que le serveur tourne normalement et que le numéro de port (par défaut : 8000) est correct. + +**Q. Les API n'apparaissent pas dans Swagger UI** +R. Vérifiez que le routeur est correctement inclus dans `src/main.py`. + +## Résumé + +Dans ce tutoriel, nous avons utilisé FastAPI-fastkit pour : + +- ✅ Créer un projet FastAPI basique +- ✅ Comprendre la structure du projet +- ✅ Utiliser des points d'extrémité d'API CRUD +- ✅ Documenter et tester l'API +- ✅ Étendre le projet + +Maintenant que vous avez appris les bases de FastAPI, lancez-vous dans des projets plus complexes ! diff --git a/docs/fr/tutorial/custom-response-handling.md b/docs/fr/tutorial/custom-response-handling.md new file mode 100644 index 0000000..e3d3dda --- /dev/null +++ b/docs/fr/tutorial/custom-response-handling.md @@ -0,0 +1,1393 @@ +# Gestion des réponses personnalisées et conception d'API avancée + +Apprenez à implémenter des formats de réponse cohérents, la gestion des erreurs, la pagination et la documentation OpenAPI personnalisée grâce aux fonctionnalités avancées de FastAPI. Nous allons mettre en œuvre des motifs de conception d'API de niveau entreprise avec le modèle `fastapi-custom-response`. + +## Ce que vous apprendrez dans ce tutoriel + +- Concevoir des formats de réponse d'API standardisés +- Gestion globale des exceptions et réponses d'erreur personnalisées +- Implémenter des systèmes de pagination +- Fonctionnalité de filtrage et de tri +- Personnaliser la documentation OpenAPI +- Gestion des versions d'API +- Mise en cache des réponses et optimisation + +## Prérequis + +- Tutoriel [Conteneurisation Docker](docker-deployment.md) terminé +- Compréhension des principes de conception d'API REST +- Connaissance des codes d'état HTTP +- Concepts de base d'OpenAPI / Swagger + +## L'importance de réponses d'API standardisées + +### Réponses incohérentes vs standardisées + +**Format de réponse problématique :** +```json +// Success +{"id": 1, "name": "item"} + +// Error +{"detail": "Not found"} + +// List retrieval +[{"id": 1}, {"id": 2}] +``` + +**Format de réponse standardisé :** +```json +// Success +{ + "success": true, + "data": {"id": 1, "name": "item"}, + "message": "Item retrieved successfully", + "timestamp": "2024-01-01T12:00:00Z" +} + +// Error +{ + "success": false, + "error": { + "code": "ITEM_NOT_FOUND", + "message": "Item not found", + "details": {"item_id": 123} + }, + "timestamp": "2024-01-01T12:00:00Z" +} +``` + +## Étape 1 : Créer un projet de réponses personnalisées + +Créez un projet avec le modèle `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! +``` + +
+ +## Étape 2 : Analyser la structure du projet + +Examinons les fonctionnalités avancées du projet généré : + +``` +advanced-api-server/ +├── src/ +│ ├── main.py # FastAPI application +│ ├── schemas/ +│ │ ├── base.py # Base response schemas +│ │ ├── items.py # Item schemas +│ │ └── responses.py # Response format definitions +│ ├── helper/ +│ │ ├── exceptions.py # Custom exception classes +│ │ └── pagination.py # Pagination helpers +│ ├── utils/ +│ │ ├── responses.py # Response utilities +│ │ └── documents.py # OpenAPI documentation customization +│ ├── api/ +│ │ └── routes/ +│ │ └── items.py # Advanced API endpoints +│ ├── crud/ +│ │ └── items.py # CRUD logic +│ └── core/ +│ └── config.py # Configuration +└── tests/ + └── test_responses.py # Response format tests +``` + +## Étape 3 : Implémenter des schémas de réponse standardisés + +### Schéma de réponse de base (`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): + """Response status""" + SUCCESS = "success" + ERROR = "error" + WARNING = "warning" + +class ErrorDetail(BaseModel): + """Error detail information""" + 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]): + """Base response format""" + 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): + """Error response format""" + 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): + """Pagination metadata""" + 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]): + """Paginated response""" + 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): + """Validation error detail""" + field: str = Field(..., description="Validation failed field") + message: str = Field(..., description="Error message") + invalid_value: Any = Field(..., description="Invalid value") + +class ValidationErrorResponse(BaseModel): + """Validation error response""" + 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") +``` + +### Fonctions utilitaires de réponse (`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: + """Generate request tracking 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: + """Generate success response""" + 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: + """Generate error response""" + 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: + """Generate paginated response""" + pages = (total + size - 1) // size # round up calculation + 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: + """Response helper class""" + + @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 + ) +``` + +## Étape 4 : Système de gestion d'exceptions personnalisé + +### Classes d'exceptions personnalisées (`src/helper/exceptions.py`) + +```python +from typing import Optional, Dict, Any +from fastapi import HTTPException + +class BaseAPIException(HTTPException): + """Base API exception class""" + + 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): + """Validation exception""" + + 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): + """Resource not found exception""" + + 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): + """Duplicate resource exception""" + + 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): + """Business logic exception""" + + 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): + """Request limit exception""" + + 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): + """Authentication exception""" + + def __init__(self, message: str = "Authentication required"): + super().__init__( + error_code="AUTHENTICATION_REQUIRED", + message=message, + status_code=401 + ) + +class AuthorizationException(BaseAPIException): + """Authorization exception""" + + def __init__(self, message: str = "Permission denied"): + super().__init__( + error_code="INSUFFICIENT_PERMISSIONS", + message=message, + status_code=403 + ) +``` + +### Gestionnaire global d'exceptions (`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): + """Custom API exception handler""" + 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 validation exception handler""" + 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 exception handler""" + 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): + """General exception handler""" + 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 + ) +``` + +## Étape 5 : Système de pagination avancé + +### Helper de pagination (`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): + """Sort order""" + ASC = "asc" + DESC = "desc" + +class PaginationParams(BaseModel): + """Pagination parameters""" + 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): + """Filtering parameters""" + 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: + """Pagination parameters dependency""" + 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: + """Filtering parameters dependency""" + return FilterParams( + search=search, + category=category, + status=status, + date_from=date_from, + date_to=date_to + ) + +class AdvancedPaginator: + """Advanced pagination class""" + + 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]: + """Apply filters""" + filtered = self.data + + if self.filters.search: + # Filter by search term (example: search in name or description fields) + 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] + + # Implement date filtering (if date field exists) + 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]: + """Apply date filter""" + 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]: + """Apply sorting""" + 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 original data if sort field is not found or cannot be sorted + return self.filtered_data + + def get_page(self) -> tuple[List[Any], int]: + """Return current page data and total count""" + 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]: + """Return pagination metadata""" + 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 + } +``` + +## Étape 6 : Implémenter des points d'extrémité d'API avancés + +### Routeur d'API d'items (`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: + """ + Create a new item + + - **name**: Item name (required) + - **description**: Item description (optional) + - **price**: Price (required, 0 or greater) + - **category**: Category (optional) + """ + # Check for duplicates + existing_item = await crud.get_by_name(item_create.name) + if existing_item: + raise DuplicateResourceException("Item", "name", item_create.name) + + # Business logic validation + if item_create.price < 0: + raise ValidationException("Price must be 0 or greater", "price") + + # Create item + created_item = await crud.create(item_create) + + # Background tasks (e.g. sending notifications, logging, etc.) + 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: + """ + Get item list (pagination, filtering, sorting supported) + + **Pagination:** + - page: Page number (default: 1) + - size: Page size (default: 20, maximum: 100) + + **Sorting:** + - sort_by: Sort field (name, price, created_at, etc.) + - sort_order: Sort order (asc, desc) + + **Filtering:** + - search: Search term (search in name or description fields) + - category: Category filter + - status: Status filter + - date_from: Start date (YYYY-MM-DD) + - date_to: End date (YYYY-MM-DD) + """ + # Get all items + all_items = await crud.get_all() + + # Apply advanced pagination + paginator = AdvancedPaginator(all_items, pagination, filters) + page_data, total = paginator.get_page() + + # Include additional metadata in response + metadata = paginator.get_metadata() + + # Create custom message + 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: + """ + Advanced search functionality + + - **q**: Search term (required) + - **fields**: Search fields list + - **exact_match**: Exact match + - **case_sensitive**: Case sensitive + """ + results = await crud.advanced_search( + query=q, + fields=fields, + exact_match=exact_match, + case_sensitive=case_sensitive + ) + + # Apply pagination + 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: + """Get specific 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: + """Update item""" + existing_item = await crud.get_by_id(item_id) + if not existing_item: + raise ResourceNotFoundException("Item", item_id) + + # Check for duplicate name (with other items) + 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: + """Delete item""" + item = await crud.get_by_id(item_id) + if not item: + raise ResourceNotFoundException("Item", item_id) + + # Validation before deletion (e.g. related orders) + 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: + """Bulk create items""" + 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: + # Check for duplicates + 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 creation notification (background task)""" + # In actual implementation, send notification via email, Slack, etc. + import asyncio + await asyncio.sleep(1) # Simulation + print(f"Item {item_id} creation notification sent") +``` + +## Étape 7 : Personnalisation de la documentation OpenAPI + +### Personnalisation de la documentation 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]: + """Create custom OpenAPI schema""" + if app.openapi_schema: + return app.openapi_schema + + openapi_schema = get_openapi( + title=app.title, + version=app.version, + description=app.description, + routes=app.routes, + ) + + # Ajouter des informations personnalisées + 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" + }) + + # Ajouter les informations sur les serveurs + 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" + } + ] + + # Ajouter un schéma de réponse commun + 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"} + } + } + }) + + # Ajouter des groupes de tags et leurs descriptions + 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" + } + ] + + # Ajouter le schéma de sécurité + 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): + """Configurer la documentation""" + app.openapi = lambda: custom_openapi(app) + + # Configurer Swagger UI + app.docs_url = "/docs" + app.redoc_url = "/redoc" + + # Ajouter un point d'accès supplémentaire pour la documentation + @app.get("/openapi.json", include_in_schema=False) + async def get_openapi_endpoint(): + return custom_openapi(app) +``` + +### Appliquer à l'application principale (ajout dans `src/main.py`) + +```python +from src.utils.documents import setup_docs +from src.api.routes import items + +# Inclure le routeur +app.include_router(items.router, prefix="/api/v1") + +# Appliquer la configuration de la documentation +setup_docs(app) + +# Ajouter un middleware d'identifiant de requête +@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 +``` + +## Étape 8 : Implémenter un système de cache + +### Mise en cache des réponses (`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: + """Memory-based cache""" + + def __init__(self): + self._cache: Dict[str, Dict[str, Any]] = {} + + async def get(self, key: str) -> Optional[Any]: + """Get value from cache""" + 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): + """Save value to cache""" + self._cache[key] = { + "value": value, + "expires_at": datetime.utcnow() + timedelta(seconds=ttl_seconds), + "created_at": datetime.utcnow() + } + + async def delete(self, key: str): + """Delete value from cache""" + self._cache.pop(key, None) + + async def clear(self): + """Delete all cache""" + self._cache.clear() + + def get_stats(self) -> Dict[str, Any]: + """Cache statistics""" + 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 + } + +# Global cache instance +cache = MemoryCache() + +def cache_response(ttl_seconds: int = 300, key_prefix: str = ""): + """Response caching decorator""" + def decorator(func): + @wraps(func) + async def wrapper(*args, **kwargs): + # Generate cache key + cache_key = generate_cache_key(func.__name__, args, kwargs, key_prefix) + + # Get from cache + cached_response = await cache.get(cache_key) + if cached_response: + return cached_response + + # Execute function + response = await func(*args, **kwargs) + + # Cache response + 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: + """Generate cache key""" + # Generate unique key based on function name and arguments + 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}" + +# Cache management endpoint +@app.get("/admin/cache/stats") +async def get_cache_stats(): + """Get cache statistics""" + stats = cache.get_stats() + return success_response(data=stats, message="Cache statistics retrieved") + +@app.delete("/admin/cache/clear") +async def clear_cache(): + """Delete all cache""" + await cache.clear() + return success_response(message="Cache deleted successfully") +``` + +### Exemple d'application du cache + +```python +# Apply caching to 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 minute caching +async def list_items( + pagination: PaginationParams = Depends(pagination_params), + filters: FilterParams = Depends(filter_params) +) -> JSONResponse: + # ... existing code ... + +@router.get("/{item_id}", response_model=dict) +@cache_response(ttl_seconds=300, key_prefix="item_detail") # 5 minute caching +async def get_item(item_id: int = Path(..., gt=0)) -> JSONResponse: + # ... existing code ... +``` + +## Étape 9 : Tester l'API + +### Lancer le serveur et tests basiques + +
+ +```console +$ cd advanced-api-server +$ fastkit runserver +Starting FastAPI server at 127.0.0.1:8000... + +# Custom response format test +$ 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" +} + +# Pagination and filtering test +$ curl "http://localhost:8000/api/v1/items/?page=1&size=10&search=notebook&sort_by=price&sort_order=desc" + +# Advanced search test +$ curl "http://localhost:8000/api/v1/items/search/advanced?q=notebook&fields=name&fields=description&exact_match=false" + +# Error response test +$ 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" +} +``` + +
+ +### Consultation de la documentation OpenAPI + +Rendez-vous sur http://localhost:8000/docs pour voir la documentation d'API personnalisée. + +## Étapes suivantes + +Vous avez terminé le système de gestion des réponses personnalisées ! Étapes suivantes à essayer : + +1. **[Intégration MCP](mcp-integration.md)** — implémenter le Model Context Protocol + + + + +## Résumé + +Dans ce tutoriel, nous avons implémenté un système avancé de gestion des réponses : + +- ✅ Conçu des formats de réponse d'API standardisés +- ✅ Gestion globale des exceptions et réponses d'erreur personnalisées +- ✅ Systèmes avancés de pagination et de filtrage +- ✅ Personnalisation de la documentation OpenAPI +- ✅ Mise en cache des réponses et optimisation des performances +- ✅ Système de suivi des requêtes +- ✅ Traitement des tâches en arrière-plan +- ✅ API d'opérations par lots + +Vous pouvez désormais implémenter toutes les fonctionnalités essentielles des serveurs d'API de niveau entreprise ! diff --git a/docs/fr/tutorial/database-integration.md b/docs/fr/tutorial/database-integration.md new file mode 100644 index 0000000..741244b --- /dev/null +++ b/docs/fr/tutorial/database-integration.md @@ -0,0 +1,1027 @@ +# Intégration de base de données (PostgreSQL + SQLAlchemy) + +Construisez une application FastAPI utilisant la base de données PostgreSQL et l'ORM SQLAlchemy, exploitable dans un véritable environnement de production. Dans ce tutoriel, nous allons implémenter un système d'intégration de base de données complet avec le modèle `fastapi-psql-orm`. + +## Ce que vous apprendrez dans ce tutoriel + +- Configurer et intégrer une base de données PostgreSQL +- Modéliser des données avec l'ORM SQLAlchemy +- Migrer des bases de données avec Alembic +- Mettre en place un environnement de développement avec Docker Compose +- Gérer un pool de connexions à la base de données +- Traiter les transactions et préserver l'intégrité des données + +## Prérequis + +- Tutoriel [API CRUD asynchrone](async-crud-api.md) terminé +- Docker et Docker Compose installés +- Connaissances de base de PostgreSQL +- Compréhension des concepts de base de l'ORM SQLAlchemy + +## Pourquoi PostgreSQL et SQLAlchemy ? + +### Comparaison fichiers JSON vs PostgreSQL + +| Catégorie | Fichiers JSON | PostgreSQL | +|----------|------------|------------| +| **Performance** | Limitée | Indexation haute performance | +| **Concurrence** | Problèmes de verrouillage de fichier | Prise en charge des transactions | +| **Évolutivité** | Limitée par la mémoire | Traitement de grandes quantités de données | +| **Intégrité** | Non garantie | Propriétés ACID garanties | +| **Requêtes** | Doit charger toutes les données | Prise en charge de requêtes complexes | +| **Sauvegarde** | Copie de fichiers | Sauvegarde / restauration complète | + +## Étape 1 : Créer un projet PostgreSQL + ORM + +Créez un projet avec le modèle `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! +``` + +
+ +## Étape 2 : Analyser la structure du projet + +Le projet généré fournit un environnement d'intégration de base de données complet : + +``` +todo-postgres-api/ +├── docker-compose.yml # Configuration du conteneur PostgreSQL +├── Dockerfile # Conteneur de l'application +├── alembic.ini # Configuration Alembic +├── template-config.yml # Configuration du template +├── scripts/ +│ ├── pre-start.sh # Initialisation avant démarrage +│ └── test.sh # Script d'exécution des tests +├── src/ +│ ├── main.py # Application FastAPI +│ ├── core/ +│ │ ├── config.py # Configuration de l'environnement +│ │ └── db.py # Configuration de la connexion à la base +│ ├── api/ +│ │ ├── deps.py # Injection de dépendances +│ │ └── routes/ +│ │ └── items.py # Points d'extrémité de l'API +│ ├── crud/ +│ │ └── items.py # Opérations de base de données +│ ├── schemas/ +│ │ └── items.py # Modèles Pydantic +│ ├── utils/ +│ │ ├── backend_pre_start.py # Initialisation du backend +│ │ ├── init_data.py # Chargement des données initiales +│ │ └── tests_pre_start.py # Préparation des tests +│ └── alembic/ +│ ├── env.py # Configuration de l'environnement Alembic +│ └── versions/ # Fichiers de migration +└── tests/ + ├── conftest.py # Configuration des tests + └── test_items.py # Tests de l'API +``` + +### Composants centraux + +1. **SQLModel** : intégration SQLAlchemy + Pydantic +2. **Alembic** : migration de schéma de base de données +3. **asyncpg** : pilote PostgreSQL asynchrone +4. **Docker Compose** : conteneurisation de l'environnement de développement + +## Étape 3 : Comprendre la configuration de la base de données + +### Configuration de la connexion (`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 + +# Create asynchronous PostgreSQL engine +engine = create_async_engine( + settings.DATABASE_URL, + echo=settings.DEBUG, # Output SQL logs + pool_size=20, # Connection pool size + max_overflow=0, # Number of additional connections allowed + pool_pre_ping=True, # Check connection status +) + +# Asynchronous session factory +AsyncSessionLocal = sessionmaker( + autocommit=False, + autoflush=False, + bind=engine, + class_=AsyncSession, + expire_on_commit=False, +) + +async def create_tables(): + """Create database tables""" + async with engine.begin() as conn: + await conn.run_sync(SQLModel.metadata.create_all) + +async def get_session() -> AsyncSession: + """Provide database session (for dependency injection)""" + async with AsyncSessionLocal() as session: + try: + yield session + finally: + await session.close() +``` + +### Configuration d'environnement (`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" + + # Configuration de la base de données + POSTGRES_SERVER: str = "localhost" + POSTGRES_USER: str = "postgres" + POSTGRES_PASSWORD: str = "password" + POSTGRES_DB: str = "todoapp" + POSTGRES_PORT: int = 5432 + + # Base de données de test + TEST_DATABASE_URL: Optional[str] = None + + # Mode debug + DEBUG: bool = False + + @property + def DATABASE_URL(self) -> str: + """Generate PostgreSQL connection 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() +``` + +## Étape 4 : Définir le modèle de données + +### Modèle de données avec SQLModel (`src/schemas/items.py`) + +```python +from sqlmodel import SQLModel, Field +from typing import Optional +from datetime import datetime + +# Define common fields +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) + +# Database table model +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) + + # Set index + class Config: + schema_extra = { + "example": { + "name": "notebook", + "description": "High-performance gaming notebook", + "price": 1500000.0, + "tax": 150000.0, + "is_active": True + } + } + +# API request/response model +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] +``` + +## Étape 5 : Implémenter les opérations CRUD + +### Logique CRUD de base de données (`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: + """Create new 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]: + """Get item by ID""" + 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]: + """Get multiple items (pagination supported)""" + 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]: + """Update item""" + # Prepare update data + update_data = item_update.dict(exclude_unset=True) + if update_data: + update_data["updated_at"] = datetime.utcnow() + + # Execute update + 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: + """Delete item (soft delete)""" + 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: + """Delete item completely""" + 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]: + """Search 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: + """Get total item count""" + 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() +``` + +## Étape 6 : Implémenter les points d'extrémité d'API + +### Configuration de l'injection de dépendances (`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]: + """Database session dependency""" + async for session in get_session(): + yield session + +def get_item_crud(db: AsyncSession = Depends(get_db)) -> ItemCRUD: + """Item CRUD dependency""" + return ItemCRUD(db) +``` + +### Implémentation du routeur d'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) +): + """Create new 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) +): + """Get item list (pagination supported)""" + 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) +): + """Search 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) +): + """Get total item count""" + 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) +): + """Get specific 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) +): + """Update 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) +): + """Delete 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" + ) +``` + +## Étape 7 : Lancer le conteneur Docker + +### Vérifier la configuration 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: +``` + +### Lancer les conteneurs + +
+ +```console +$ cd todo-postgres-api + +# Start service in background +$ 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 + +# Check service status +$ 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 + +# Check log +$ docker-compose logs app +``` + +
+ +## Étape 8 : Migration de la base de données + +### Créer la migration initiale avec Alembic + +
+ +```console +# Run migration inside container +$ 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 + +# Apply migration +$ 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 +``` + +
+ +### Inspecter le fichier de migration + +Consultez le fichier de migration créé : + +```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 ### +``` + +## Étape 9 : Tester l'API + +### Test CRUD basique + +
+ +```console +# Create new 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 +} + +# Get item list +$ curl "http://localhost:8000/items/" + +# Get item list with pagination +$ curl "http://localhost:8000/items/?skip=0&limit=10" + +# Search item +$ curl "http://localhost:8000/items/search?q=MacBook" + +# Get item count +$ curl "http://localhost:8000/items/count" +{"total": 1} +``` + +
+ +### Test des fonctionnalités de requête avancées + +
+ +```console +# Get item list with inactive items +$ curl "http://localhost:8000/items/?active_only=false" + +# Update item +$ curl -X PUT "http://localhost:8000/items/1" \ + -H "Content-Type: application/json" \ + -d '{ + "price": 2300000, + "tax": 230000 + }' + +# Soft delete item +$ curl -X DELETE "http://localhost:8000/items/1" + +# Hard delete item +$ curl -X DELETE "http://localhost:8000/items/1?hard_delete=true" +``` + +
+ +## Étape 10 : Fonctionnalités avancées de base de données + +### Traitement des transactions + +```python +# À ajouter dans `src/crud/items.py` + +from sqlalchemy.exc import SQLAlchemyError + +async def create_items_batch(self, items_create: List[ItemCreate]) -> List[Item]: + """Créer plusieurs items dans une transaction""" + 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() + + # Rafraîchir tous les items + for item in created_items: + await self.db.refresh(item) + + return created_items + + except SQLAlchemyError: + await self.db.rollback() + raise +``` + +### Modélisation de données relationnelles + +```python +# À ajouter dans `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 + + # Définir la relation + 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) + + # Ajouter la clé étrangère + category_id: Optional[int] = Field(foreign_key="categories.id") + + # Définir la relation + category: Optional[Category] = Relationship(back_populates="items") +``` + +### Optimisation des index + +```python +# À ajouter dans `src/schemas/items.py` + +from sqlalchemy import Index + +class Item(ItemBase, table=True): + __tablename__ = "items" + + # ... existing fields ... + + # Set composite index + __table_args__ = ( + Index('ix_items_price_active', 'price', 'is_active'), + Index('ix_items_created_at', 'created_at'), + Index('ix_items_name_description', 'name', 'description'), # For full text search + ) +``` + +## Étape 11 : Écrire des tests + +### Configuration des tests de base de données (`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 + +# Moteur de base de données de test +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(): + # Créer les tables de test + async with test_engine.begin() as conn: + await conn.run_sync(SQLModel.metadata.create_all) + + # Fournir une session + async with TestSessionLocal() as session: + yield session + + # Supprimer les tables après le test + async with test_engine.begin() as conn: + await conn.run_sync(SQLModel.metadata.drop_all) + +@pytest.fixture +async def client(db_session: AsyncSession): + # Remplacer la dépendance + 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 d'intégration (`tests/test_items.py`) + +```python +import pytest +from httpx import AsyncClient + +@pytest.mark.asyncio +async def test_create_and_read_item(client: AsyncClient): + """Test d'intégration pour créer puis lire un item""" + # Créer un 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 + + # Récupérer l'item créé + 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): + """Tester la pagination""" + # Créer plusieurs items + 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) + + # Récupérer la première page + response = await client.get("/items/?skip=0&limit=10") + assert response.status_code == 200 + + items = response.json() + assert len(items) == 10 + + # Récupérer la deuxième page + 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): + """Tester la recherche""" + # Créer des items de test + 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) + + # Rechercher « 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" + + # Rechercher « smartphone » (dans la 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" +``` + +### Lancer les tests + +
+ +```console +# Lancer les tests dans le conteneur +$ 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 ======================== +``` + +
+ +## Étape 12 : Considérations pour un déploiement en production + +### Optimiser le pool de connexions + +```python +# À ajouter dans `src/core/config.py` + +class Settings(BaseSettings): + # ... paramètres existants ... + + # Réglages du pool de connexions à la base + DB_POOL_SIZE: int = 20 + DB_MAX_OVERFLOW: int = 0 + DB_POOL_PRE_PING: bool = True + DB_POOL_RECYCLE: int = 300 # 5 minutes + + # Délai maximal d'une requête + DB_QUERY_TIMEOUT: int = 30 + + # Réglages de nouvelle tentative de connexion + DB_RETRY_ATTEMPTS: int = 3 + DB_RETRY_DELAY: int = 1 +``` + +### Surveillance de la base de données + +```python +# À ajouter dans `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): + """Log before query execution""" + context._query_start_time = time.time() + +@event.listens_for(Engine, "after_cursor_execute") +def receive_after_cursor_execute(conn, cursor, statement, parameters, context, executemany): + """Log after query execution""" + total = time.time() - context._query_start_time + if total > 1.0: # Log slow queries (1 second or more) + logger.warning(f"Slow query: {total:.2f}s - {statement[:100]}...") +``` + +## Étapes suivantes + +Vous avez terminé l'intégration de la base de données PostgreSQL ! Étapes suivantes à essayer : + +1. **[Conteneurisation Docker](docker-deployment.md)** — construire un environnement de déploiement en production +2. **[Gestion des réponses personnalisées](custom-response-handling.md)** — formats de réponse d'API avancés + + + +## Résumé + +Dans ce tutoriel, nous avons utilisé PostgreSQL et SQLAlchemy pour : + +- ✅ Intégrer la base de données PostgreSQL +- ✅ Implémenter l'ORM avec SQLModel +- ✅ Mettre en place le système de migration Alembic +- ✅ Opérations CRUD avancées et optimisation des requêtes +- ✅ Traitement des transactions et intégrité des données +- ✅ Fonctionnalités de pagination, recherche et tri +- ✅ Tests d'intégration et tests de base de données +- ✅ Considérations pour un déploiement en production + +Vous pouvez désormais construire des API robustes pilotées par base de données, utilisables dans de véritables environnements de production ! diff --git a/docs/fr/tutorial/docker-deployment.md b/docs/fr/tutorial/docker-deployment.md new file mode 100644 index 0000000..6f362bb --- /dev/null +++ b/docs/fr/tutorial/docker-deployment.md @@ -0,0 +1,1177 @@ +# Conteneurisation Docker et déploiement + +Apprenez à conteneuriser des applications FastAPI avec Docker pour bâtir des environnements de développement cohérents et préparer un déploiement en production. Nous allons mettre en place un environnement de déploiement complet basé sur Docker en utilisant le modèle `fastapi-dockerized`. + +## Ce que vous apprendrez dans ce tutoriel + +- Conteneuriser des applications FastAPI avec Docker +- Créer des images Docker optimisées avec des builds multi-étapes +- Mettre en place des environnements de développement avec Docker Compose +- Configurer Docker pour un déploiement en production +- Surveiller les conteneurs et gérer les journaux +- Construire des pipelines CI/CD + +## Prérequis + +- Tutoriel [Intégration de base de données](database-integration.md) terminé +- Docker et Docker Compose installés +- Maîtrise des commandes Docker de base +- Connaissances de base des concepts de conteneurs + +## Avantages de la conteneurisation Docker + +### Approche traditionnelle vs Docker + +| Catégorie | Approche traditionnelle | Approche Docker | +|----------|---------------------|-----------------| +| **Cohérence de l'environnement** | Différences entre environnements | Même environnement partout | +| **Gestion des dépendances** | Installation manuelle requise | Toutes les dépendances incluses dans l'image | +| **Vitesse de déploiement** | Lente | Déploiement rapide possible | +| **Évolutivité** | Limitée | Mise à l'échelle facile | +| **Rollback** | Complexe | Rollback immédiat vers une version antérieure | +| **Utilisation des ressources** | Lourde | Conteneurs légers | + +## Étape 1 : Créer un projet basé sur Docker + +Créez un projet avec le modèle `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! +``` + +
+ +## Étape 2 : Analyser les fichiers de configuration Docker + +Examinons les fichiers liés à Docker dans le projet généré : + +``` +dockerized-todo-api/ +├── Dockerfile # Docker image build configuration +├── docker-compose.yml # Development environment container setup +├── docker-compose.prod.yml # Production environment configuration +├── .dockerignore # Files to exclude during Docker build +├── scripts/ +│ ├── start.sh # Container startup script +│ ├── prestart.sh # Pre-start initialization script +│ └── gunicorn.conf.py # Gunicorn configuration +├── src/ +│ ├── main.py # FastAPI application +│ └── ... # Other source code +└── requirements.txt # Python dependencies +``` + +### Analyse du Dockerfile + +```dockerfile +# Optimized Dockerfile using multi-stage build + +# ============================================ +# Stage 1: Build stage +# ============================================ +FROM python:3.12-slim as builder + +# Install build tools +RUN apt-get update && apt-get install -y \ + build-essential \ + curl \ + && rm -rf /var/lib/apt/lists/* + +# Copy dependency file and install +COPY requirements.txt . +RUN pip install --user --no-cache-dir -r requirements.txt + +# ============================================ +# Stage 2: Runtime stage +# ============================================ +FROM python:3.12-slim + +# System update and essential package installation +RUN apt-get update && apt-get install -y \ + curl \ + && rm -rf /var/lib/apt/lists/* \ + && apt-get clean + +# Create non-root user (security enhancement) +RUN groupadd -r appuser && useradd -r -g appuser appuser + +# Create application directory +WORKDIR /app + +# Copy Python packages from build stage +COPY --from=builder /root/.local /home/appuser/.local + +# Copier le code de l'application +COPY . . + +# Définir les permissions des fichiers +RUN chown -R appuser:appuser /app +RUN chmod +x scripts/start.sh scripts/prestart.sh + +# Ajouter le chemin des paquets Python à PATH +ENV PATH=/home/appuser/.local/bin:$PATH + +# Passer à un utilisateur non root +USER appuser + +# Configurer la vérification de santé +HEALTHCHECK --interval=30s --timeout=30s --start-period=5s --retries=3 \ + CMD curl -f http://localhost:8000/health || exit 1 + +# Expose port +EXPOSE 8000 + +# Execute startup script +CMD ["./scripts/start.sh"] +``` + +### Environnement de développement 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: + # Mount volume for development (auto-reload on code changes) + - ./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 (for caching and session store) + 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 (reverse proxy) + 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 +``` + +### Environnement de production 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 +``` + +## Étape 3 : Configurer les scripts de démarrage + +### Script de démarrage principal (`scripts/start.sh`) + +```bash +#!/bin/bash + +set -e + +# Set environment variables +export PYTHONPATH=/app:$PYTHONPATH + +# Run pre-start script +echo "Running pre-start script..." +./scripts/prestart.sh + +# Determine execution mode based on environment +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 +``` + +### Script de pré-démarrage (`scripts/prestart.sh`) + +```bash +#!/bin/bash + +set -e + +echo "Running pre-start checks..." + +# Check Python modules and dependencies +echo "Checking Python dependencies..." +python -c "import fastapi, uvicorn, pydantic; print('✓ Core dependencies OK')" + +# Check environment variables +if [[ -z "$ENVIRONMENT" ]]; then + export ENVIRONMENT="development" + echo "ℹ ENVIRONMENT not set, defaulting to development" +fi + +# Create log directory +mkdir -p /app/logs +touch /app/logs/app.log + +# Check if health endpoint is present +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!" +``` + +### Configuration Gunicorn (`scripts/gunicorn.conf.py`) + +```python +import multiprocessing +import os + +# Server socket +bind = "0.0.0.0:8000" +backlog = 2048 + +# Worker process +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 + +# Worker restart settings +preload_app = True +timeout = 120 +keepalive = 2 + +# Logging +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' + +# Process name +proc_name = "dockerized-todo-api" + +# Security +limit_request_line = 4094 +limit_request_fields = 100 +limit_request_field_size = 8190 + +# Performance tuning +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") +``` + +## Étape 4 : Implémenter une sonde de santé et la surveillance + +### Ajouter le point d'extrémité de santé (`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" +) + +# Application start time +start_time = time.time() + +@app.get("/health", status_code=status.HTTP_200_OK) +async def health_check(): + """ + Container health check endpoint + """ + current_time = time.time() + uptime = current_time - start_time + + # System resource information + 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(), + } + } + + # Check if all checks passed + 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: + """Check database connection status""" + try: + # In actual implementation, test database connection + return True + except Exception: + return False + +async def check_redis_connection() -> bool: + """Check Redis connection status""" + try: + # In actual implementation, test Redis connection + return True + except Exception: + return False + +def check_disk_space() -> bool: + """Check disk space""" + disk_usage = psutil.disk_usage('/') + free_percentage = (disk_usage.free / disk_usage.total) * 100 + return free_percentage > 10 # 10% or more free space needed + +@app.get("/health/ready", status_code=status.HTTP_200_OK) +async def readiness_check(): + """ + Kubernetes readiness probe endpoint + """ + # Check if application is ready to receive traffic + return {"status": "ready", "timestamp": datetime.utcnow().isoformat()} + +@app.get("/health/live", status_code=status.HTTP_200_OK) +async def liveness_check(): + """ + Kubernetes liveness probe endpoint + """ + return {"status": "alive", "timestamp": datetime.utcnow().isoformat()} +``` + +## Étape 5 : Configurer le proxy inverse Nginx + +### Configuration Nginx pour le développement (`nginx/nginx.conf`) + +```nginx +events { + worker_connections 1024; +} + +http { + upstream fastapi_backend { + # Specify backend by container name + server app:8000; + } + + # Define log format + 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; + + # Default settings + sendfile on; + tcp_nopush on; + tcp_nodelay on; + keepalive_timeout 65; + types_hash_max_size 2048; + client_max_body_size 100M; + + # Gzip compression + 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; + + # Security headers + add_header X-Content-Type-Options nosniff; + add_header X-Frame-Options DENY; + add_header X-XSS-Protection "1; mode=block"; + + # Health check endpoint + 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; + + # Health check should respond quickly + proxy_connect_timeout 5s; + proxy_send_timeout 5s; + proxy_read_timeout 5s; + } + + # API endpoint + 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; + + # Timeout settings + proxy_connect_timeout 30s; + proxy_send_timeout 30s; + proxy_read_timeout 30s; + + # Buffering settings + proxy_buffering on; + proxy_buffer_size 4k; + proxy_buffers 8 4k; + } + + # Static file caching (future use) + location /static { + expires 1y; + add_header Cache-Control public; + add_header ETag ""; + } + } +} +``` + +### Configuration Nginx pour la production (`nginx/nginx.prod.conf`) + +```nginx +events { + worker_connections 2048; +} + +http { + upstream fastapi_backend { + # Load balancing for multiple app instances + server app:8000 max_fails=3 fail_timeout=30s; + # server app2:8000 max_fails=3 fail_timeout=30s; # For scaling + + # Keep-alive + keepalive 32; + } + + # Security settings + server_tokens off; + + # Rate limiting + 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 settings + 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; + + # Security headers + 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; + + # Health check (rate limit applied) + location /health { + limit_req zone=health burst=20 nodelay; + proxy_pass http://fastapi_backend; + include /etc/nginx/proxy_params; + } + + # API endpoint (rate limit applied) + location / { + limit_req zone=api burst=20 nodelay; + proxy_pass http://fastapi_backend; + include /etc/nginx/proxy_params; + } + } +} +``` + +## Étape 6 : Construire et lancer les conteneurs + +### Lancer en environnement de développement + +
+ +```console +$ cd dockerized-todo-api + +# Build Docker image +$ 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 + +# Run container (background) +$ 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 + +# Check container status +$ 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 +``` + +
+ +### Consulter les journaux + +
+ +```console +# Check all service logs +$ docker-compose logs + +# Check specific service logs +$ docker-compose logs app +$ docker-compose logs nginx +$ docker-compose logs redis + +# Check real-time logs +$ docker-compose logs -f app +``` + +
+ +### Tester la sonde de santé + +
+ +```console +# Basic health check +$ 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 probe test +$ curl http://localhost/health/ready +$ curl http://localhost/health/live +``` + +
+ +## Étape 7 : Déploiement en production + +### Définir les variables d'environnement (`.env.prod`) + +```bash +# Application settings +ENVIRONMENT=production +DEBUG=false +SECRET_KEY=your-super-secret-key-here +WORKERS=4 + +# Database settings +DATABASE_URL=postgresql://user:password@db:5432/todoapp +REDIS_URL=redis://:password@redis:6379/0 +REDIS_PASSWORD=your-redis-password + +# Logging settings +LOG_LEVEL=info +LOG_FILE=/app/logs/app.log + +# Security settings +ALLOWED_HOSTS=["your-domain.com"] +CORS_ORIGINS=["https://your-frontend.com"] + +# Monitoring +SENTRY_DSN=https://your-sentry-dsn@sentry.io/project-id +``` + +### Commandes de déploiement en production + +
+ +```console +# Deploy in production environment +$ docker-compose -f docker-compose.prod.yml --env-file .env.prod up -d + +# Scaling (app instance scaling) +$ docker-compose -f docker-compose.prod.yml up -d --scale app=3 + +# Rolling update +$ docker-compose -f docker-compose.prod.yml build app +$ docker-compose -f docker-compose.prod.yml up -d --no-deps app + +# Safe shutdown before backup +$ docker-compose -f docker-compose.prod.yml down --timeout 30 +``` + +
+ +## Étape 8 : Surveillance et journalisation + +### Surveillance des ressources des conteneurs Docker + +
+ +```console +# Check real-time resource usage +$ 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 + +# Check specific container details +$ docker inspect dockerized-todo-api + +# Check container internal processes +$ docker-compose exec app ps aux +``` + +
+ +### Agrégation et analyse des journaux + +```yaml +# docker-compose.logging.yml +version: '3.8' + +services: + # ELK Stack for log aggregation + 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 for log collection + 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 +``` + +### Collecte de métriques Prometheus + +```python +# src/monitoring.py +from prometheus_client import Counter, Histogram, Gauge, generate_latest +from fastapi import Request, Response +import time + +# Define metrics +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 metric collection middleware""" + 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 metric endpoint""" + return Response(generate_latest(), media_type="text/plain") +``` + +## Étape 9 : Construire un pipeline CI/CD + +### Workflow 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 + + # Pull new image + docker-compose -f docker-compose.prod.yml pull + + # Rolling update + docker-compose -f docker-compose.prod.yml up -d --no-deps app + + # Health check + sleep 30 + curl -f http://localhost/health || exit 1 + + # Clean up previous image + docker image prune -f +``` + +## Étape 10 : Renforcer la sécurité + +### Paramètres de sécurité des conteneurs + +```dockerfile +# Ajouter des renforcements de sécurité au Dockerfile + +# Exécuter le conteneur avec un utilisateur non root +USER appuser + +# Système de fichiers racine en lecture seule +# docker run --read-only --tmpfs /tmp dockerized-todo-api + +# Limiter les permissions +# docker run --cap-drop=ALL dockerized-todo-api + +# Isolation réseau +# docker run --network=none dockerized-todo-api +``` + +### Paramètres de sécurité Docker Compose + +```yaml +# Ajouter des paramètres de sécurité à docker-compose.yml +services: + app: + # ... paramètres existants ... + security_opt: + - no-new-privileges:true + cap_drop: + - ALL + cap_add: + - NET_BIND_SERVICE + read_only: true + tmpfs: + - /tmp + - /app/logs + user: "1000:1000" +``` + +### Gestion des secrets + +```yaml +# Ajouter la gestion des secrets à docker-compose.yml +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 +``` + +## Étapes suivantes + +Vous avez terminé la conteneurisation Docker ! Étapes suivantes à essayer : + +1. **[Gestion des réponses personnalisées](custom-response-handling.md)** — implémenter des formats de réponse d'API avancés + + + + +## Résumé + +Dans ce tutoriel, nous avons utilisé Docker pour : + +- ✅ Créer des images de conteneur optimisées avec des builds multi-étapes +- ✅ Mettre en place des environnements de développement / production avec Docker Compose +- ✅ Configurer un proxy inverse Nginx et la répartition de charge +- ✅ Construire des systèmes de sonde de santé et de surveillance +- ✅ Implémenter un déploiement automatisé via des pipelines CI/CD +- ✅ Configurer des paramètres de sécurité de niveau production +- ✅ Implémenter des systèmes de journalisation et de collecte de métriques + +Vous pouvez désormais déployer des applications FastAPI de façon sûre et efficace dans des environnements de production ! diff --git a/docs/fr/tutorial/domain-starter.md b/docs/fr/tutorial/domain-starter.md new file mode 100644 index 0000000..7a27b52 --- /dev/null +++ b/docs/fr/tutorial/domain-starter.md @@ -0,0 +1,393 @@ +# FastAPI orienté domaine avec `fastapi-domain-starter` + +Construisez un service FastAPI de taille moyenne avec l'organisation moderne recommandée — **un dossier par concept métier** sous `src/app/domains/`. Ce tutoriel vous guide de bout en bout dans le modèle `fastapi-domain-starter` : comment le générer, à quoi sert chaque package de premier niveau, comment l'exemple `items` fourni est relié au reste de l'application, et comment ajouter votre prochain domaine. + +## Ce que vous apprendrez + +- Générer un projet avec `fastkit startdemo fastapi-domain-starter` +- Le rôle de `core`, `db`, `domains` et `tests` dans la disposition +- Comment un domaine s'organise en `router → service → repository → schemas → models` +- Le contrat pour ajouter un nouveau domaine (copier le dossier items, enregistrer le routeur) +- Comment le point d'extrémité `/health` fourni et le CRUD `/api/v1/items` s'intègrent à l'application + +## Prérequis + +- Python 3.12+ +- FastAPI-fastkit installé (`pip install fastapi-fastkit`) +- À l'aise avec les concepts FastAPI de base (opérations de chemin, schémas pydantic, dépendances) + +S'il s'agit de votre premier projet FastAPI, commencez plutôt par [Construire un serveur d'API basique](basic-api-server.md) — ce tutoriel utilise le modèle plus simple `fastapi-default`. + +## Étape 1 : Générer le projet + +```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` déploie le modèle, remplit les emplacements, crée un environnement virtuel et installe les dépendances. Une fois terminé, plongez-y : + +```console +$ cd orders-api +$ bash scripts/run-server.sh # or: uvicorn src.app.main:app --reload +``` + +La documentation de l'API est ensuite servie à . + +## Étape 2 : L'arborescence générée + +``` +orders-api/ +├── README.md +├── pyproject.toml # PEP 621 metadata + [tool.fastapi-fastkit] +├── requirements.txt # pinned deps (template ships both files; you maintain them as you add packages) +├── .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/ # le package applicatif +│ ├── __init__.py +│ ├── main.py # FastAPI() + middleware + inclusion de api_router +│ ├── core/ # configuration transverse +│ │ ├── __init__.py +│ │ └── config.py # pydantic-settings (PROJECT_NAME, CORS, ...) +│ ├── db/ # abstractions de persistance +│ │ ├── __init__.py +│ │ └── memory.py # stockage clé-valeur générique InMemoryStore[T] +│ ├── api/ # routage côté transport +│ │ ├── __init__.py +│ │ ├── health.py # GET /health +│ │ └── router.py # regroupe la santé et tous les routeurs de domaine +│ └── domains/ # concepts métier (un dossier chacun) +│ ├── __init__.py +│ └── items/ # le domaine d'exemple +│ ├── __init__.py +│ ├── models.py # @dataclass Item (entité) +│ ├── schemas.py # ItemCreate, ItemRead (Pydantic) +│ ├── repository.py # ItemRepository au-dessus de InMemoryStore +│ ├── service.py # ItemService + ItemNotFoundError +│ └── router.py # APIRouter(prefix="/items") +└── tests/ + ├── __init__.py + ├── conftest.py # fixture TestClient, remise à zéro du store + ├── test_health.py + └── test_items.py +``` + +Les deux idées à intégrer : + +1. **`src/app/`** est le **package applicatif** — tout ce que l'application charge à l'exécution se trouve ici. Les tests importent depuis ce package (`from src.app.main import app`). Le `src/` externe sert à rendre le projet installable via `pip install`. +2. **`src/app/domains//`** représente une **découpe par concept** — chaque concept métier (items, orders, users, …) possède son propre `router / service / repository / schemas / models`, et rien de plus. + +## Étape 3 : Le rôle de chaque package de premier niveau + +### `src/app/core/` — configuration + +Contient la configuration applicative transversale. Le `config.py` fourni expose une classe `Settings` basée sur pydantic-settings, lue depuis `.env` / les variables d'environnement : + +```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` lit `settings.PROJECT_NAME`, `settings.API_V1_PREFIX` et `settings.all_cors_origins` pour câbler l'application FastAPI. + +**Quand ajouter dans `core/` :** tout ce qui n'est pas spécifique à un seul domaine — paramètres globaux, journalisation structurée, middlewares personnalisés, helpers de sécurité, etc. + +### `src/app/db/` — frontière de persistance + +Contient l'abstraction sur votre magasin de données. Le starter fournit `memory.py` — un `InMemoryStore[T]` local au processus, générique sur le type d'entité. Le repository de chaque domaine enveloppe un `InMemoryStore`, de sorte que basculer plus tard sur SQLAlchemy / un pilote asynchrone est un changement circonscrit : seuls les repositories doivent être réécrits. + +```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: ... +``` + +**Quand étoffer `db/` :** ajoutez un `session.py` avec votre vraie fabrique de sessions de base de données une fois que vous migrez hors d'`InMemoryStore`. Conservez la signature publique des méthodes (`list` / `get` / `add` / …) inchangée pour que les repositories des domaines n'aient pas à modifier leur contrat interne. + +### `src/app/api/` — routage de transport + +Deux pièces : + +- `health.py` — un petit `APIRouter` exposant `GET /health` qui renvoie `{"status": "ok"}`. Sans effet de bord, idéal pour les sondes de vivacité. +- `router.py` — l'**agrégateur de premier niveau**. Il inclut le routeur de santé et le routeur de chaque domaine, et cet `api_router` combiné unique est monté sur l'application FastAPI sous `/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) +``` + +**Pourquoi agréger ici :** lorsque vous ajoutez un nouveau domaine, vous ne modifiez que `src/app/api/router.py` pour enregistrer son routeur. `main.py` ne change jamais. + +### `src/app/domains//` — tranches métier + +C'est là que vit l'essentiel de votre code à mesure que le projet grossit. Chaque domaine possède cinq fichiers : + +| Fichier | Rôle | +|---|---| +| `models.py` | Entité du domaine (un `@dataclass` dans le starter ; pourrait être SQLAlchemy / SQLModel plus tard). La forme interne — pas le format de transport. | +| `schemas.py` | Schémas d'E/S de l'API (pydantic). Séparés de l'entité pour que le format de transport puisse évoluer sans toucher à la logique du domaine. | +| `repository.py` | Accès aux données. Enveloppe le magasin avec des méthodes typées pour l'item. La couture où la persistance est échangée. | +| `service.py` | Logique métier. Les routeurs appellent le `service`, jamais directement le `repository`. Les exceptions spécifiques au domaine (par ex. `ItemNotFoundError`) vivent ici. | +| `router.py` | Transport HTTP. Traduit les schémas pydantic ↔ appels au service ; convertit les exceptions du domaine en `HTTPException`. | + +Le **sens des dépendances** suit `router → service → repository → store`. Chaque couche ne dépend que de celle qui se trouve juste en dessous. Les schémas sont utilisés par le `router` et le `service` ; les `models` sont utilisés par le `repository` et le `service`. + +### `tests/` + +Le dossier reflète la structure de l'application à l'exécution : un module de test par surface de comportement importante. Le starter fournit : + +- `conftest.py` — fixture autouse qui réinitialise le magasin d'items entre les tests, plus une fixture `client` qui enveloppe `TestClient(app)`. +- `test_health.py` — vérifie que `GET /api/v1/health` renvoie 200 + `{"status": "ok"}`. +- `test_items.py` — couverture CRUD complète des points d'extrémité items, y compris un 404 pour des ids inconnus et un 422 pour une charge utile invalide. + +Lancez avec : + +```console +$ bash scripts/test.sh # or: pytest +``` + +## Étape 4 : Parcourir le domaine `items` fourni + +Le domaine d'exemple est un CRUD sur une petite entité : + +```python +# src/app/domains/items/models.py +@dataclass +class Item: + id: int + name: str + price: float + in_stock: bool = True +``` + +Les schémas d'API séparent la forme d'entrée de la forme de sortie afin de pouvoir ajouter des champs contrôlés par le serveur (`id`) et de la validation (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) +``` + +Le repository enveloppe le magasin en mémoire et attribue les ids à l'insertion : + +```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 elided +``` + +La couche service est l'endroit où s'accumulent les règles métier. Aujourd'hui, c'est un mince passe-plat avec une exception personnalisée, mais c'est ici que vivra la politique future (« on ne peut pas supprimer un item présent dans une commande ouverte », etc.) : + +```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 elided +``` + +Le routeur est la seule pièce qui connaît HTTP. Notez qu'il prend le service sous forme de `Depends(...)` FastAPI pour que les tests puissent le surcharger, et qu'il associe `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)) +``` + +Le routeur complet expose : + +| Méthode | Chemin | Ce qu'il fait | +|---|---|---| +| `GET` | `/api/v1/items` | Lister les items | +| `GET` | `/api/v1/items/{item_id}` | Lire un item | +| `POST` | `/api/v1/items` | Créer (renvoie 201) | +| `PUT` | `/api/v1/items/{item_id}` | Remplacer | +| `DELETE` | `/api/v1/items/{item_id}` | Supprimer (renvoie 204) | +| `GET` | `/api/v1/health` | Sonde de vivacité | + +Essayez : + +```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"} +``` + +## Étape 5 : Ajouter votre prochain domaine + +Le starter est conçu pour que **l'ajout d'un domaine reste surtout une opération de copie puis de renommage**. Supposons que vous vouliez un domaine `users` aux côtés de `items` : + +### 1. Copier le dossier `items/` + +```console +$ cp -r src/app/domains/items src/app/domains/users +``` + +### 2. Réécrire l'entité, les schémas et les noms de classe de chaque fichier + +```python +# src/app/domains/users/models.py +from dataclasses import dataclass + +@dataclass +class User: + id: int + email: str + is_active: bool = True +``` + +```python +# src/app/domains/users/schemas.py +from pydantic import BaseModel, ConfigDict, Field + +class UserCreate(BaseModel): + # Garder ``str`` permet de réutiliser l'exemple tel quel. + # Si vous préférez la validation d'e-mail intégrée à Pydantic, + # installez la dépendance optionnelle + # (``pip install 'pydantic[email]'`` — qui ajoute ``email-validator``) + # puis remplacez ``str`` par ``EmailStr``. + email: str = Field(min_length=3, max_length=320) + is_active: bool = True + +class UserRead(BaseModel): + id: int + email: str + is_active: bool + model_config = ConfigDict(from_attributes=True) +``` + +Renommez `Item → User`, `ItemNotFoundError → UserNotFoundError`, `ItemRepository → UserRepository`, `ItemService → UserService` dans `models.py`, `schemas.py`, `repository.py`, `service.py` et `router.py`. N'oubliez pas `prefix="/items"` → `prefix="/users"` et `tags=["items"]` → `tags=["users"]` dans le routeur. + +Le repository peut conserver le même motif appuyé sur `InMemoryStore` — il est générique sur le type d'entité : + +```python +# src/app/domains/users/repository.py +_store: InMemoryStore[User] = InMemoryStore() + +class UserRepository: + def __init__(self, store: Optional[InMemoryStore[User]] = None) -> None: + self._store = store if store is not None else _store + # ... same shape as ItemRepository ... +``` + +### 3. Mettre à jour le `__init__.py` du domaine + +Le domaine items réexporte ses modules pour que les appelants puissent écrire `from src.app.domains.items import service`. Faites pareil pour users : + +```python +# src/app/domains/users/__init__.py +from src.app.domains.users import ( # noqa: F401 + models, + repository, + router, + schemas, + service, +) +``` + +### 4. Enregistrer le routeur dans l'agrégateur + +C'est le **seul fichier en dehors de `domains/users/` que vous devez toucher** : + +```python +# src/app/api/router.py +from src.app.api import health +from src.app.domains.items import router as items_router +from src.app.domains.users import router as users_router # ← add + +api_router = APIRouter() +api_router.include_router(health.router) +api_router.include_router(items_router.router) +api_router.include_router(users_router.router) # ← add +``` + +Après un redémarrage du serveur, vous verrez `/api/v1/users` monté dans `/docs`. + +### 5. Ajouter des tests + +Calquez `tests/test_items.py` en `tests/test_users.py` — même forme orientée client, pointant simplement sur les nouveaux points d'extrémité. La fixture autouse de réinitialisation du magasin dans `conftest.py` isole déjà chaque test. + +Si vous ajoutez un deuxième domaine qui utilise aussi `InMemoryStore`, élargissez la fixture pour réinitialiser également son magasin, ou conservez une fixture par domaine. + +## Étape 6 : Aller plus loin + +- La [Matrice des préréglages d'architecture](../reference/preset-feature-matrix.md) montre ce que `fastkit init --interactive` génère pour chaque préréglage, y compris les sélections de fonctionnalités qui demandent encore un peu de câblage manuel avec `domain-starter`. +- Le [tutoriel `fastapi-default`](basic-api-server.md) couvre l'alternative en couches si vous souhaitez comparer les dispositions avant de vous engager. +- Pour l'intégration de base de données, le [tutoriel Intégration de base de données](database-integration.md) présente le motif PostgreSQL + SQLAlchemy + Alembic. On retrouve les mêmes idées dans `src/app/db/` et dans les fichiers `repository.py` de chaque domaine. + +## Récapitulatif + +- **Génération** : `fastkit startdemo fastapi-domain-starter` → `bash scripts/run-server.sh` → documentation à `/docs`. +- **Disposition** : `core/` pour la configuration, `db/` pour les abstractions de persistance, `domains//` pour les tranches métier, `api/router.py` comme point d'agrégation unique, `tests/` qui reflète les modules d'exécution. +- **Ajouter un domaine** : copier `items/`, renommer entité / schémas / classes, mettre à jour les réexportations de `__init__.py`, enregistrer le routeur dans `src/app/api/router.py`, ajouter un module de test. Aucune édition dans `main.py`. diff --git a/docs/fr/tutorial/first-project.md b/docs/fr/tutorial/first-project.md new file mode 100644 index 0000000..e1da807 --- /dev/null +++ b/docs/fr/tutorial/first-project.md @@ -0,0 +1,1252 @@ +# Votre premier projet + +Construisez une API de blog complète avec gestion des utilisateurs, création de billets et système de commentaires en utilisant FastAPI-fastkit. + +## Vue d'ensemble du projet + +Dans ce tutoriel, nous allons créer une **API de blog** avec les fonctionnalités suivantes : + +- **Gestion des utilisateurs** : inscription, authentification et profils utilisateur +- **Gestion des billets** : créer, lire, mettre à jour et supprimer des billets de blog +- **Système de commentaires** : ajouter des commentaires aux billets +- **Validation des données** : validation robuste des entrées et gestion des erreurs +- **Documentation d'API** : documentation OpenAPI automatique +- **Tests** : suite de tests complète + +### Ce que vous apprendrez + +À la fin de ce tutoriel, vous comprendrez : + +- La structure avancée d'un projet FastAPI-fastkit +- L'intégration de base de données avec SQLAlchemy +- L'authentification et l'autorisation des utilisateurs +- Les relations de données complexes +- La gestion des erreurs et la validation +- Les bonnes pratiques de tests + +## Prérequis + +Avant de commencer, assurez-vous d'avoir : + +- Terminé le tutoriel [Prise en main](getting-started.md) +- Une compréhension de base des API REST +- Python 3.12+ installé +- Un éditeur de texte ou IDE prêt + +## Étape 1 : Créer le projet + +Commençons par créer un nouveau projet avec la pile **STANDARD** pour la prise en charge des bases de données : + +
+ +```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! +``` + +
+ +## Étape 2 : Configurer le projet + +Allez dans le projet et activez l'environnement virtuel : + +
+ +```console +$ cd blog-api +$ source .venv/bin/activate +``` + +
+ +## Étape 3 : Ajouter les routes nécessaires + +Ajoutons les ressources principales de notre API de blog : + +
+ +```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' +``` + +
+ +## Étape 4 : Concevoir les modèles de données + +Concevons nos schémas de données. Commençons par mettre à jour le schéma utilisateur pour qu'il soit plus réaliste. + +### Mettre à jour le schéma utilisateur + +Modifiez `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 +``` + +### Créer le schéma de billet + +Modifiez `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"] = [] + +# Import to avoid circular imports +from src.schemas.users import User +from src.schemas.comments import Comment +PostWithAuthor.model_rebuild() +PostWithComments.model_rebuild() +``` + +### Créer le schéma de commentaire + +Modifiez `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" + +# Import to avoid circular imports +from src.schemas.users import User +CommentWithAuthor.model_rebuild() +``` + +## Étape 5 : Implémenter des opérations CRUD avancées + +### CRUD utilisateur enrichi + +Mettez à jour `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 _verify_password(self, plain_password: str, hashed_password: str) -> bool: + """Verify password against hash""" + return self._hash_password(plain_password) == hashed_password + + def get_all(self) -> List[UserInDB]: + """Get all users""" + return [user for user in self._users if user.is_active] + + def get_by_id(self, user_id: int) -> Optional[UserInDB]: + """Get user by ID""" + return next((user for user in self._users if user.id == user_id), None) + + 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, + 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]: + """Update an existing user""" + user = self.get_by_id(user_id) + if not user: + return None + + # Check for duplicates on email/username changes + 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: + """Soft delete user (deactivate)""" + 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]: + """Authenticate user by email and password""" + user = self.get_by_email(email) + if user and self._verify_password(password, user.hashed_password): + return user + return None + +users_crud = UsersCRUD() +``` + +### CRUD des billets + +Mettez à jour `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]: + """Get all posts with pagination""" + 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]: + """Get post by 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]: + """Get posts by author""" + 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: + """Create a new 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) + + # Update author's post count + 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]: + """Update an existing 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: + """Delete a post""" + post = self.get_by_id(post_id) + if post and post.author_id == author_id: + self._posts.remove(post) + + # Update author's post count + 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]: + """Search posts by title or content""" + 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 des commentaires + +Mettez à jour `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]: + """Get all comments""" + return self._comments + + def get_by_id(self, comment_id: int) -> Optional[Comment]: + """Get comment by 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]: + """Get comments for a specific post""" + 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]: + """Get comments by author""" + 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: + """Create a new comment""" + # Verify post exists + 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) + + # Update post's comment count + post.comments_count += 1 + + return new_comment + + def update(self, comment_id: int, comment_update: CommentUpdate, author_id: int) -> Optional[Comment]: + """Update an existing 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: + """Delete a comment""" + comment = self.get_by_id(comment_id) + if comment and comment.author_id == author_id: + self._comments.remove(comment) + + # Update post's comment count + 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() +``` + +## Étape 6 : Implémenter des routes d'API avancées + +### Routes utilisateur enrichies + +Mettez à jour `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() + +# Helper function to get current user (simplified for tutorial) +def get_current_user_id() -> int: + # In a real app, this would verify JWT token and return user ID + return 1 # For tutorial purposes + +@router.get("/", response_model=List[User]) +def read_users( + skip: int = Query(0, ge=0), + limit: int = Query(100, ge=1, le=100) +): + """Get all users with pagination""" + 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): + """Register a new user""" + 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): + """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()) + +@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) +): + """Update user profile""" + 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) +): + """Deactivate user account""" + 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): + """Authenticate user""" + user = users_crud.authenticate(email, password) + if not user: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Invalid email or password" + ) + + # In a real app, return JWT token + return { + "message": "Login successful", + "user_id": user.id, + "username": user.username + } +``` + +### Routes de billets enrichies + +Mettez à jour `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 # Simplified for tutorial + +@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) +): + """Get all posts with optional search""" + 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) +): + """Create a new blog post""" + 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): + """Get a specific post""" + 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) +): + """Update a blog post""" + 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) +): + """Delete a blog post""" + 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) +): + """Get posts by a specific author""" + posts = posts_crud.get_by_author(author_id, skip, limit) + return posts +``` + +### Routes de commentaires enrichies + +Mettez à jour `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 # Simplified for tutorial + +@router.get("/", response_model=List[Comment]) +def read_comments( + skip: int = Query(0, ge=0), + limit: int = Query(100, ge=1, le=100) +): + """Get all comments""" + 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) +): + """Create a new comment""" + 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): + """Get a specific comment""" + 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) +): + """Update a comment""" + 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) +): + """Delete a comment""" + 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) +): + """Get comments for a specific post""" + 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) +): + """Get comments by a specific author""" + comments = comments_crud.get_by_author(author_id, skip, limit) + return comments +``` + +## Étape 7 : Tester votre API de blog + +Démarrons le serveur et testons notre API de blog complète : + +
+ +```console +$ fastkit runserver +INFO: Uvicorn running on http://127.0.0.1:8000 +``` + +
+ +### Tester l'inscription d'un utilisateur + +
+ +```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 +} +``` + +
+ +### Tester la connexion d'un utilisateur + +
+ +```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" +} +``` + +
+ +### Tester la création d'un billet + +
+ +```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 +} +``` + +
+ +### Tester la création d'un commentaire + +
+ +```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" +} +``` + +
+ +### Tester la fonctionnalité de recherche + +
+ +```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 + } +] +``` + +
+ +## Étape 8 : Documentation de l'API + +Rendez-vous sur [http://127.0.0.1:8000/docs](http://127.0.0.1:8000/docs) pour voir la documentation complète de votre API. Vous devriez désormais voir : + +- **Users** : inscription, connexion, gestion du profil +- **Posts** : opérations CRUD, recherche, filtrage par auteur +- **Comments** : opérations CRUD, filtrage par billet / auteur +- **Items** : points d'extrémité d'exemple d'origine + +La documentation présente : + +- Tous les points d'extrémité disponibles +- Les schémas de requête / réponse +- Les règles de validation des données +- Les réponses d'erreur + +## Étape 9 : Écrire des tests + +Créons des tests complets pour notre API de blog. Créez `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 # Should not expose password + + def test_duplicate_email(self): + # First user + user_data1 = { + "email": "duplicate@example.com", + "username": "user1", + "password": "password123" + } + response1 = client.post("/api/v1/users/", json=user_data1) + assert response1.status_code == 201 + + # Second user with same email + 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): + # Create user first + user_data = { + "email": "login@example.com", + "username": "loginuser", + "password": "loginpassword123" + } + client.post("/api/v1/users/", json=user_data) + + # Tester la connexion + 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): + # Create a post with specific content + post_data = { + "title": "FastAPI Tutorial", + "content": "Learn how to build APIs with FastAPI", + "published": True + } + client.post("/api/v1/posts/", json=post_data) + + # Search for the post + 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): + # Create a post first + 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"] + + # Create comment + 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): + # Create post and comment first + 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) + + # Get comments for the post + 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) + +# Run the tests +if __name__ == "__main__": + import pytest + pytest.main([__file__]) +``` + +### Lancer les tests + +
+ +```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 ======================== +``` + +
+ +## Ce que vous avez construit + +Félicitations ! Vous avez construit avec succès une API de blog complète avec : + +### ✅ Fonctionnalités implémentées + +- **Gestion des utilisateurs** + - Inscription d'utilisateur avec validation + - Authentification des utilisateurs (connexion) + - Gestion du profil + - Prévention des doublons + +- **Billets de blog** + - Créer, lire, mettre à jour, supprimer des billets + - Filtrage par auteur + - Fonctionnalité de recherche + - Statut publié / brouillon + +- **Système de commentaires** + - Ajouter des commentaires aux billets + - Voir les commentaires par billet ou par auteur + - Gestion des commentaires + +- **Validation des données** + - Validation d'e-mail + - Exigences sur les mots de passe + - Limites de longueur du contenu + - Validation des champs requis + +- **Gestion des erreurs** + - Codes d'état HTTP appropriés + - Messages d'erreur explicites + - Erreurs de validation des entrées + +- **Documentation d'API** + - Génération automatique d'OpenAPI + - Interface de test interactive + - Schémas de requête / réponse + +- **Tests** + - Couverture de tests complète + - Tests unitaires pour tous les points d'extrémité + - Tests de cas limites + +## Étapes suivantes + +### Améliorations possibles + +1. **Authentification réelle** + - Implémenter des tokens JWT + - Ajouter le hachage de mot de passe avec bcrypt + - Permissions basées sur les rôles + +2. **Intégration de base de données** + - Utiliser PostgreSQL ou MySQL + - Implémenter de vrais modèles de base de données + - Ajouter des migrations de base de données + +3. **Fonctionnalités avancées** + - Téléversement de fichiers pour les images + - Notifications par e-mail + - Catégories / étiquettes de billets + - Système de like / dislike + +4. **Préparation à la production** + - Ajouter de la journalisation + - Implémenter la mise en cache + - Ajouter de la limitation de débit + - Configuration d'environnement + +### Continuer à apprendre + +1. **[Utiliser les modèles](../user-guide/using-templates.md)** : explorer le modèle `fastapi-psql-orm` pour l'intégration de base de données +2. **[Ajouter des routes](../user-guide/adding-routes.md)** : apprendre des motifs de routage plus avancés +3. **[Contribution](../contributing/development-setup.md)** : contribuer à FastAPI-fastkit + +!!! tip "Bonnes pratiques que vous avez apprises" + - **Architecture modulaire** : séparation des préoccupations entre schémas, CRUD et routes + - **Validation des données** : utilisation de Pydantic pour une validation robuste des entrées + - **Gestion des erreurs** : codes d'état HTTP et messages d'erreur appropriés + - **Tests** : couverture de tests complète pour toutes les fonctionnalités + - **Documentation** : exploitation de la génération automatique de documentation d'API + +Vous avez maintenant les compétences pour construire des API de qualité production avec FastAPI-fastkit ! 🚀 diff --git a/docs/fr/tutorial/getting-started.md b/docs/fr/tutorial/getting-started.md new file mode 100644 index 0000000..1b75380 --- /dev/null +++ b/docs/fr/tutorial/getting-started.md @@ -0,0 +1,564 @@ +# Prise en main + +Un tutoriel complet et pas à pas pour démarrer avec FastAPI-fastkit. Ce guide vous mènera de l'installation à l'exécution de votre première API en 15 minutes environ. + +## Prérequis + +Avant de commencer, assurez-vous d'avoir : + +- **Python 3.12 ou supérieur** installé sur votre système +- **Une connaissance de base de Python** (variables, fonctions, classes) +- **L'accès au terminal / à la ligne de commande** +- **Un éditeur de texte ou un IDE** (VS Code, PyCharm, etc.) + +## Étape 1 : Installation + +Commençons par installer FastAPI-fastkit. Nous recommandons d'utiliser un environnement virtuel pour isoler vos projets. + +### Option A : avec pip (traditionnel) + +
+ +```console +$ pip install fastapi-fastkit +---> 100% +Successfully installed fastapi-fastkit +``` + +
+ +### Option B : avec UV (recommandé — plus rapide) + +UV est un gestionnaire de paquets Python rapide. Si vous n'avez pas UV installé : + +
+ +```console +# Installez d'abord UV +$ curl -LsSf https://astral.sh/uv/install.sh | sh + +# Installez ensuite FastAPI-fastkit +$ uv pip install fastapi-fastkit +---> 100% +Successfully installed fastapi-fastkit +``` + +
+ +### Option C : avec un environnement virtuel + +
+ +```console +$ python -m venv fastapi-env +$ source fastapi-env/bin/activate # On Windows: fastapi-env\Scripts\activate +$ pip install fastapi-fastkit +``` + +
+ +### Vérifier l'installation + +Vérifiez que FastAPI-fastkit est correctement installé : + +
+ +```console +$ fastkit --version +FastAPI-fastkit version 1.0.0 +``` + +
+ +## Étape 2 : Créer votre premier projet + +Créons maintenant votre premier projet FastAPI avec la commande interactive `init` : + +
+ +```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 "Sélection de la pile" + Nous avons choisi **MINIMAL** pour ce tutoriel afin de garder les choses simples. Pour de vrais projets, envisagez **STANDARD** (inclut la prise en charge des bases de données) ou **FULL** (inclut les tâches en arrière-plan). + +## Étape 3 : Aller dans votre projet + +Déplacez-vous dans le répertoire du projet nouvellement créé : + +
+ +```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 +``` + +
+ +## Étape 4 : Activer l'environnement virtuel + +Votre projet est livré avec un environnement virtuel préconfiguré. Activez-le : + +
+ +```console +$ source .venv/bin/activate # Sous Windows : .venv\Scripts\activate +(my-first-api) $ +``` + +
+ +Notez que votre invite de terminal affiche désormais `(my-first-api)`, ce qui indique que l'environnement virtuel est actif. + +## Étape 5 : Démarrer le serveur de développement + +Voici la partie excitante — démarrons votre serveur 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. +``` + +
+ +🎉 **Félicitations !** Votre serveur FastAPI tourne désormais. + +## Étape 6 : Tester votre API + +Testons votre API de plusieurs manières : + +### Méthode 1 : navigateur + +Ouvrez votre navigateur web et visitez : + +- **Point d'extrémité principal de l'API** : [http://127.0.0.1:8000](http://127.0.0.1:8000) + +Vous devriez voir : +```json +{"message": "Hello World"} +``` + +### Méthode 2 : documentation interactive de l'API + +Visitez la documentation d'API générée automatiquement : + +- **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 est particulièrement utile — vous pouvez : + +- Voir tous les points d'extrémité disponibles +- Tester les points d'extrémité directement dans votre navigateur +- Consulter les schémas de requête / réponse +- Télécharger les spécifications OpenAPI + +### Méthode 3 : ligne de commande + +Ouvrez un nouveau terminal (gardez le serveur en marche) et testez avec 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" +} +``` + +
+ +## Étape 7 : Comprendre la structure de votre projet + +Explorons ce que FastAPI-fastkit a généré pour vous : + +
+ +```console +$ tree src +src/ +├── __init__.py +├── main.py # Point d'entrée de l'application FastAPI +├── core/ +│ ├── __init__.py +│ └── config.py # Configuration de l'application +├── api/ +│ ├── __init__.py +│ ├── api.py # Routeur API principal +│ └── routes/ +│ ├── __init__.py +│ └── items.py # Points d'extrémité de l'API des items +├── crud/ +│ ├── __init__.py +│ └── items.py # Logique métier des items +├── schemas/ +│ ├── __init__.py +│ └── items.py # Schémas de validation des données +└── mocks/ + ├── __init__.py + └── mock_items.json # Données d'exemple +``` + +
+ +### Fichiers clés expliqués + +**`src/main.py`** — le cœur de votre application : +```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`** — paramètres de l'application : +```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`** — points d'extrémité d'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) +``` + +## Étape 8 : Ajouter votre première route personnalisée + +Ajoutons une nouvelle route d'API pour pratiquer ce que vous avez appris : + +
+ +```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' +``` + +
+ +Le serveur va redémarrer automatiquement et vous disposez maintenant de nouveaux points d'extrémité : + +- `GET /api/v1/users/` — récupérer tous les utilisateurs +- `POST /api/v1/users/` — créer un nouvel utilisateur +- `GET /api/v1/users/{user_id}` — récupérer un utilisateur précis +- Et plus encore… + +### Tester votre nouvelle route + +
+ +```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" + } +] +``` + +
+ +## Étape 9 : Explorer et modifier le code + +Faisons une petite modification pour comprendre comment fonctionne le code. + +### Modifier le message d'accueil + +Ouvrez `src/main.py` dans votre éditeur de texte et modifiez le point d'extrémité racine : + +```python +@app.get("/") +def read_root(): + return {"message": "Welcome to my first FastAPI application!"} +``` + +Enregistrez le fichier. Grâce au rechargement automatique, votre serveur redémarre automatiquement. + +### Tester la modification + +
+ +```console +$ curl http://127.0.0.1:8000 +{"message":"Welcome to my first FastAPI application!"} +``` + +
+ +### Ajouter un nouveau point d'extrémité + +Ajoutons un point d'extrémité simple à `src/main.py` : + +```python +@app.get("/hello/{name}") +def say_hello(name: str): + return {"message": f"Hello, {name}!"} +``` + +### Tester le nouveau point d'extrémité + +
+ +```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!"} +``` + +
+ +## Étape 10 : Lancer les tests + +Votre projet est livré avec des tests préconfigurés. Lançons-les : + +
+ +```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 ======================== +``` + +
+ +## Comprendre les concepts clés + +### 1. Structure de l'application FastAPI + +FastAPI-fastkit suit une **architecture modulaire** : + +- **`main.py`** : point d'entrée de l'application et points d'extrémité globaux +- **`api/`** : organisation des routes d'API +- **`core/`** : configuration et paramètres de l'application +- **`crud/`** : logique métier et opérations sur les données +- **`schemas/`** : validation et sérialisation des données +- **`tests/`** : tests automatisés + +### 2. Gestion des dépendances + +Votre projet utilise une gestion moderne des dépendances Python : + +- **Environnement virtuel** : environnement Python isolé +- **requirements.txt** : liste toutes les dépendances +- **Installation automatique** : dépendances installées lors de la création du projet + +### 3. Serveur de développement + +FastAPI-fastkit utilise **Uvicorn** comme serveur ASGI : + +- **Rechargement automatique** : redémarre automatiquement lors des changements de code +- **Démarrage rapide** : itération de développement rapide +- **Prêt pour la production** : même serveur que celui utilisé en production + +### 4. Documentation d'API + +FastAPI génère automatiquement : + +- **Spécification OpenAPI** : documentation d'API conforme aux standards de l'industrie +- **Swagger UI** : interface de test interactive +- **ReDoc** : vue alternative de la documentation + +## Étapes suivantes + +Félicitations ! Vous avez : + +✅ Installé FastAPI-fastkit +✅ Créé votre premier projet +✅ Démarré le serveur de développement +✅ Testé vos points d'extrémité d'API +✅ Ajouté une nouvelle route +✅ Modifié du code existant +✅ Lancé les tests + +### Continuer à apprendre + +1. **[Votre premier projet](first-project.md)** : construire une API de blog complète avec des fonctionnalités avancées +2. **[Ajouter des routes](../user-guide/adding-routes.md)** : apprendre à créer des points d'extrémité d'API complexes +3. **[Utiliser les modèles](../user-guide/using-templates.md)** : explorer les modèles de projet prêts à l'emploi + +### Expérimenter davantage + +Essayez ces défis : + +1. **Ajouter de la validation** : modifier les schémas pour ajouter des règles de validation des données +2. **Réponses personnalisées** : changer les formats de réponse dans les routes +3. **Variables d'environnement** : utiliser des fichiers `.env` pour la configuration +4. **Ajouter un middleware** : implémenter CORS ou l'authentification +5. **Intégration de base de données** : passer à la pile STANDARD pour la prise en charge des bases de données + +### Problèmes courants et solutions + +**Le serveur ne démarre pas :** + +- Vérifiez que vous êtes dans le répertoire du projet +- Assurez-vous que l'environnement virtuel est activé +- Vérifiez l'absence d'erreurs de syntaxe dans votre code + +**Erreurs d'import :** + +- Assurez-vous que tous les fichiers `__init__.py` existent +- Vérifiez que vos chemins d'import sont corrects +- Vérifiez que vous utilisez l'environnement virtuel + +**Port déjà utilisé :** +```console +$ fastkit runserver --port 8080 +``` + +## Bonnes pratiques que vous avez apprises + +1. **Environnements virtuels** : toujours utiliser des environnements isolés +2. **Structure de projet** : suivre une architecture modulaire organisée +3. **Rechargement automatique** : utiliser le serveur de développement pour une itération rapide +4. **Documentation d'API** : exploiter la génération automatique de documentation +5. **Tests** : exécuter les tests régulièrement pendant le développement + +!!! tip "Astuces de développement" + - Gardez le serveur de développement en marche pendant que vous codez + - Utilisez la documentation interactive (`/docs`) pour tester vos API + - Consultez le terminal pour des messages d'erreur utiles + - Committez votre code dans un système de contrôle de version régulièrement + +Vous êtes maintenant prêt à construire de superbes API avec FastAPI-fastkit ! 🚀 diff --git a/docs/fr/tutorial/mcp-integration.md b/docs/fr/tutorial/mcp-integration.md new file mode 100644 index 0000000..a93ddd7 --- /dev/null +++ b/docs/fr/tutorial/mcp-integration.md @@ -0,0 +1,1730 @@ +# Intégration MCP (Model Context Protocol) + +Apprenez à intégrer le Model Context Protocol (MCP) à FastAPI pour construire un système où les modèles d'IA peuvent utiliser les points d'extrémité d'API comme des outils. Nous allons implémenter une API complète intégrée à l'IA, incluant l'authentification, la gestion des permissions et l'implémentation d'un serveur MCP avec le modèle `fastapi-mcp`. + +## Ce que vous apprendrez dans ce tutoriel + +- Concepts et implémentation du Model Context Protocol (MCP) +- Construire un système d'authentification basé sur JWT +- Implémenter le contrôle d'accès basé sur les rôles (RBAC) +- Exposer et gérer des outils MCP +- Communication sécurisée d'API avec des modèles d'IA +- Gestion des sessions et du contexte utilisateur + +## Prérequis + +- Tutoriel [Gestion des réponses personnalisées](custom-response-handling.md) terminé +- Compréhension des concepts de base de JWT et OAuth2 +- Concepts de communication d'API avec les modèles d'IA / LLM +- Connaissances de base du protocole MCP + +## Qu'est-ce que le Model Context Protocol (MCP) ? + +MCP est un protocole standardisé qui permet aux modèles d'IA d'interagir avec des systèmes externes. + +### Approche traditionnelle vs MCP + +**Approche traditionnelle (appels d'API directs) :** +``` +AI Model → HTTP Request → API Server → Response +``` + +**Approche MCP :** +``` +AI Model → MCP Client → MCP Server (FastAPI) → Safe Tool Execution → Response +``` + +### Avantages de MCP + +- **Sécurité** : authentification et gestion des permissions intégrées +- **Standardisation** : fourniture d'une interface cohérente +- **Gestion du contexte** : maintien de l'état basé sur les sessions +- **Abstraction des outils** : exposer des API complexes en tant qu'outils simples + +## Étape 1 : Créer un projet d'intégration MCP + +Créez un projet avec le modèle `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! +``` + +
+ +## Étape 2 : Analyse de la structure du projet + +Examinons la structure du projet généré : + +``` +ai-integrated-api/ +├── src/ +│ ├── main.py # FastAPI application +│ ├── auth/ +│ │ ├── __init__.py +│ │ ├── models.py # Authentication-related data models +│ │ ├── jwt_handler.py # JWT token processing +│ │ ├── dependencies.py # Authentication dependencies +│ │ └── routes.py # Authentication router +│ ├── mcp/ +│ │ ├── __init__.py +│ │ ├── server.py # MCP server implementation +│ │ ├── tools.py # MCP tool definitions +│ │ └── client.py # MCP client (for testing) +│ ├── api/ +│ │ ├── __init__.py +│ │ ├── api.py # Regroupement des routeurs API +│ │ └── routes/ +│ │ ├── items.py # Item management API +│ │ ├── users.py # User management API +│ │ └── admin.py # Admin API +│ ├── schemas/ +│ │ ├── __init__.py +│ │ ├── auth.py # Authentication schemas +│ │ ├── users.py # User schemas +│ │ └── items.py # Item schemas +│ └── core/ +│ ├── __init__.py +│ ├── config.py # Configuration +│ ├── database.py # Database (in-memory) +│ └── security.py # Security configuration +└── tests/ + ├── test_auth.py # Authentication tests + ├── test_mcp.py # MCP tests + └── test_integration.py # Integration tests +``` + +## Étape 3 : Implémentation du système d'authentification + +### Traitement des tokens 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 + +# Password hashing +pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") + +def verify_password(plain_password: str, hashed_password: str) -> bool: + """Password verification""" + return pwd_context.verify(plain_password, hashed_password) + +def get_password_hash(password: str) -> str: + """Password hashing""" + return pwd_context.hash(password) + +def create_access_token(data: Dict[str, Any], expires_delta: Optional[timedelta] = None) -> str: + """Access token generation""" + 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: + """Refresh token generation""" + 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]]: + """Token decoding""" + 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]: + """Token verification and user ID return""" + payload = decode_token(token) + + if not payload: + return None + + # Token type verification + 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: + """Token management class""" + + def __init__(self): + self.blacklisted_tokens = set() + + def blacklist_token(self, token: str): + """Add token to blacklist""" + self.blacklisted_tokens.add(token) + + def is_blacklisted(self, token: str) -> bool: + """Check if token is blacklisted""" + return token in self.blacklisted_tokens + + def create_token_pair(self, user_id: str, user_role: str) -> Dict[str, str]: + """Create access/refresh token pair""" + 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" + } + +# Global token manager +token_manager = TokenManager() +``` + +### Modèles d'utilisateur et base de données (`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): + """User roles""" + ADMIN = "admin" + USER = "user" + AI_AGENT = "ai_agent" + READONLY = "readonly" + +class Permission(str, Enum): + """Permissions""" + 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): + """User model""" + 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 # For MCP client + +class UserInDB(User): + """User model for database storage""" + hashed_password: str + +class UserCreate(BaseModel): + """User creation schema""" + email: EmailStr + username: str + password: str + full_name: Optional[str] = None + role: UserRole = UserRole.USER + +class UserUpdate(BaseModel): + """User update schema""" + 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): + """Login request schema""" + username: str + password: str + +class TokenResponse(BaseModel): + """Token response schema""" + access_token: str + refresh_token: str + token_type: str = "bearer" + expires_in: int + user: User + +# Default permission mapping by role +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: + """Memory-based user database""" + + def __init__(self): + self.users: Dict[str, UserInDB] = {} + self._init_default_users() + + def _init_default_users(self): + """Create default users""" + from src.auth.jwt_handler import get_password_hash + import uuid + + # Admin account + 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 agent account + 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]: + """Get user by username""" + 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]: + """Get user by ID""" + return self.users.get(user_id) + + def get_user_by_api_key(self, api_key: str) -> Optional[UserInDB]: + """Get user by API key""" + return next( + (user for user in self.users.values() if user.api_key == api_key), + None + ) + + def create_user(self, user_create: UserCreate) -> UserInDB: + """Create user""" + 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]: + """Update user""" + 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) + + # Update permissions if role changed + if "role" in update_data: + user.permissions = ROLE_PERMISSIONS[user.role] + + return user + + def update_last_login(self, user_id: str): + """Update last login time""" + if user_id in self.users: + self.users[user_id].last_login = datetime.utcnow() + +# Global database instance +user_db = UserDatabase() +``` + +## Étape 4 : Implémentation des dépendances d'authentification + +### Dépendances d'authentification (`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 schema +security = HTTPBearer() +api_key_header = APIKeyHeader(name="X-API-Key", auto_error=False) + +async def get_current_user( + credentials: HTTPAuthorizationCredentials = Security(security) +) -> User: + """Get current authenticated user""" + credentials_exception = HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Could not validate credentials", + headers={"WWW-Authenticate": "Bearer"}, + ) + + try: + token = credentials.credentials + + # Check blacklist + 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]: + """Authenticate user by API key""" + 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: + """Authenticate user by token or API key (flexible authentication)""" + 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): + """Dependency requiring specific permissions""" + 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): + """Dependency requiring specific 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 + +# Common permission dependencies +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) +``` + +### Routeur d'authentification (`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): + """Register user""" + # Check duplicate username + if user_db.get_user_by_username(user_create.username): + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="Username already registered" + ) + + # First user is automatically set as admin + 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 login""" + 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" + ) + + # Create token + tokens = token_manager.create_token_pair(user.id, user.role) + + # Update last login time + 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): + """Refresh token""" + 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" + ) + + # Create new token pair + 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)): + """User logout""" + # In actual implementation, add token to blacklist + return {"message": "Successfully logged out"} + +@router.get("/me", response_model=User) +async def get_current_user_info(current_user: User = Depends(get_current_user)): + """Get current user information""" + return current_user + +@router.put("/me", response_model=User) +async def update_current_user( + user_update: UserUpdate, + current_user: User = Depends(get_current_user) +): + """Update current user information""" + # Normal users cannot change role + 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)): + """Get user list (admin only)""" + 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) +): + """Create user API key (admin only)""" + 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" + ) + + # Create new API key + new_api_key = str(uuid.uuid4()) + user.api_key = new_api_key + + return { + "api_key": new_api_key, + "message": "API key generated successfully" + } +``` + +## Étape 5 : Implémentation du serveur MCP + +### Définition des outils 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): + """Tool category""" + DATA_MANAGEMENT = "data_management" + SEARCH = "search" + ANALYSIS = "analysis" + ADMIN = "admin" + +class MCPTool(BaseModel): + """MCP tool definition""" + 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: + """Tool registry""" + + def __init__(self): + self.tools: Dict[str, MCPTool] = {} + self._register_default_tools() + + def _register_default_tools(self): + """Register default tools""" + + # Create item tool + 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" + } + ] + )) + + # Search item tool + 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 + } + ] + )) + + # Analyze item tool + 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" + } + } + ] + )) + + # Manage user tool (admin only) + 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): + """Register tool""" + self.tools[tool.name] = tool + + def get_tool(self, tool_name: str) -> Optional[MCPTool]: + """Get tool""" + return self.tools.get(tool_name) + + def list_tools(self, user_permissions: List[str] = None) -> List[MCPTool]: + """List tools by user permissions""" + if user_permissions is None: + return list(self.tools.values()) + + available_tools = [] + for tool in self.tools.values(): + # Check permissions + 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]: + """List tools by category""" + tools = self.list_tools(user_permissions) + return [tool for tool in tools if tool.category == category] + +# Global tool registry +tool_registry = ToolRegistry() +``` + +### Implémentation du serveur 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 server""" + + def __init__(self): + self.item_crud = ItemCRUD() + self.active_sessions: Dict[str, Dict[str, Any]] = {} + + async def create_session(self, user: User) -> str: + """Create MCP session""" + 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]]: + """Get session""" + session = self.active_sessions.get(session_id) + if session: + session["last_activity"] = datetime.utcnow() + return session + + async def close_session(self, session_id: str): + """Close session""" + if session_id in self.active_sessions: + del self.active_sessions[session_id] + + async def list_tools(self, user: User) -> List[Dict[str, Any]]: + """List tools available to user""" + 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]: + """Execute tool""" + + # Check if tool exists + 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" + ) + + # Check permissions + 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}'" + ) + + # Update session + if session_id: + session = await self.get_session(session_id) + if session: + session["tool_usage_count"] += 1 + + # Execute tool + 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: + """Execute tool logic""" + + 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]: + """Create item tool implementation""" + 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]: + """Search item tool implementation""" + 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) + + # Search logic implementation + all_items = await self.item_crud.get_all() + filtered_items = [] + + for item in all_items: + # Text search + if query.lower() not in item.name.lower() and query.lower() not in (item.description or "").lower(): + continue + + # Category filter + if category and getattr(item, 'category', None) != category: + continue + + # Price filter + 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 limit + 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]: + """Analyze item tool implementation""" + 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]: + """Manage user tool implementation""" + action = parameters.get("action") + + # Check admin permissions + 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}") + +# Global MCP server instance +mcp_server = MCPServer() +``` + +## Étape 6 : Implémentation des points d'extrémité d'API MCP + +### Routeur d'API MCP (`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 execution request""" + tool_name: str + parameters: Dict[str, Any] + session_id: Optional[str] = None + +class SessionCreateResponse(BaseModel): + """Session creation response""" + session_id: str + message: str + +@router.post("/session", response_model=SessionCreateResponse) +async def create_mcp_session( + current_user: User = Depends(RequireMCPTools) +): + """Create MCP session""" + 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) +): + """Close MCP session""" + session = await mcp_server.get_session(session_id) + + if not session: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Session not found" + ) + + # Check session owner + 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) +): + """List available MCP tools""" + 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) +): + """Execute MCP tool""" + + # Check session (optional) + 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" + ) + + # Execute tool + result = await mcp_server.execute_tool( + tool_name=request.tool_name, + parameters=request.parameters, + user=current_user, + session_id=request.session_id + ) + + # Log tool usage in background + 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) +): + """List active user sessions""" + 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 usage statistics""" + 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): + """Log tool usage (background job)""" + import logging + + logger = logging.getLogger("mcp.usage") + logger.info( + f"Tool usage - User: {user_id}, Tool: {tool_name}, Success: {success}" + ) +``` + +## Étape 7 : Intégration applicative et tests + +### Application principale (`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 settings +app.add_middleware( + CORSMiddleware, + allow_origins=settings.ALLOWED_HOSTS, + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + +# Inclure les routeurs +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(): + """Health check endpoint""" + return { + "status": "healthy", + "version": "1.0.0", + "services": { + "auth": "operational", + "mcp": "operational", + "database": "operational" + } + } +``` + +### Lancer le serveur et tester + +
+ +```console +$ cd ai-integrated-api +$ fastkit runserver +Starting FastAPI server at 127.0.0.1:8000... + +# User login +$ 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", ...] + } +} + +# Create MCP session +$ 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)" +} + +# List available tools +$ 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": [...] + }, + ... + ] +} + +# Execute MCP tool (create 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" +} + +# Execute MCP tool (search 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 + } + }' +``` + +
+ +## Étape 8 : Exemple de client IA + +### Exemple de client MCP en Python + +```python +# client_example.py +import asyncio +import aiohttp +from typing import Dict, Any, List + +class MCPClient: + """MCP client example""" + + 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: + """Create MCP session""" + 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): + """Close MCP session""" + 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]]: + """List available tools""" + 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]: + """Execute tool""" + 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 assistant workflow simulation""" + + # 1. Create session + await self.create_session() + print(f"Session created: {self.session_id}") + + # 2. Analyze user request and select appropriate tool + if "Create item" in user_request or "Create" in user_request: + # Create item request + 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 request + search_query = "Item" # Actually extracted from 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: + # Analyze 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(): + """Client test""" + async with MCPClient("http://localhost:8000", "your-api-key-here") as client: + + # List available tools + 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 assistant simulation + 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()) +``` + + + + + +## Résumé + +Dans ce tutoriel, nous avons implémenté l'intégration MCP (Model Context Protocol) avec : + +- ✅ Construction d'un système d'authentification basé sur JWT +- ✅ Implémentation du contrôle d'accès basé sur les rôles (RBAC) +- ✅ Implémentation d'un serveur MCP et d'un système d'outils +- ✅ Gestion du contexte basée sur les sessions +- ✅ Communication sécurisée d'API avec des modèles d'IA +- ✅ Gestion des permissions sur les outils et suivi de l'usage +- ✅ Implémentation d'un véritable exemple de client IA + +Vous pouvez désormais construire un système complet basé sur MCP, dans lequel les modèles d'IA peuvent utiliser de manière sûre et efficace les fonctionnalités de votre API ! diff --git a/docs/fr/user-guide/adding-routes.md b/docs/fr/user-guide/adding-routes.md new file mode 100644 index 0000000..cadc5d9 --- /dev/null +++ b/docs/fr/user-guide/adding-routes.md @@ -0,0 +1,581 @@ +# Ajouter des routes + +Apprenez à ajouter de nouvelles routes d'API à votre projet FastAPI existant. + +## Ajout de route basique + +### Utiliser la commande `addroute` + +La commande `addroute` de FastAPI-fastkit facilite l'ajout de nouvelles routes : + +
+ +```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` │ +╰───────────────────────────────────────────────────────╯ +``` + +
+ +## Ce qui est créé + +Lorsque vous ajoutez une route, FastAPI-fastkit crée automatiquement : + +### 1. Fichier de route : `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. Opérations 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. Schémas 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. Enregistrement du routeur + +La commande met automatiquement à jour `src/api/api.py` pour inclure le nouveau routeur : + +```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"]) +``` + +## Points d'extrémité d'API générés + +Après avoir ajouté la route `users`, vous disposerez de ces points d'extrémité : + +| Méthode | Point d'extrémité | Description | +|--------|----------|-------------| +| `GET` | `/api/v1/users/` | Récupérer tous les utilisateurs | +| `POST` | `/api/v1/users/` | Créer un nouvel utilisateur | +| `GET` | `/api/v1/users/{user_id}` | Récupérer un utilisateur précis | +| `PUT` | `/api/v1/users/{user_id}` | Mettre à jour un utilisateur | +| `DELETE` | `/api/v1/users/{user_id}` | Supprimer un utilisateur | + +## Tester vos nouvelles routes + +### 1. Démarrer le serveur + +
+ +```console +$ fastkit runserver +INFO: Uvicorn running on http://127.0.0.1:8000 +``` + +
+ +### 2. Consulter la documentation de l'API + +Rendez-vous sur [http://127.0.0.1:8000/docs](http://127.0.0.1:8000/docs) pour voir vos nouveaux points d'extrémité dans la documentation interactive. + +### 3. Tester avec curl + +**Créer un utilisateur :** +
+ +```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" +} +``` + +
+ +**Récupérer tous les utilisateurs :** +
+ +```console +$ curl http://127.0.0.1:8000/api/v1/users/ + +[ + { + "id": 1, + "title": "John Doe", + "description": "Software Developer" + } +] +``` + +
+ +**Récupérer un utilisateur précis :** +
+ +```console +$ curl http://127.0.0.1:8000/api/v1/users/1 + +{ + "id": 1, + "title": "John Doe", + "description": "Software Developer" +} +``` + +
+ +## Personnaliser le code généré + +Le code généré est entièrement personnalisable. Voici des modifications courantes : + +### 1. Schéma utilisateur enrichi + +Modifiez `src/schemas/users.py` pour des données utilisateur plus réalistes : + +```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 enrichi avec validation + +Mettez à jour `src/crud/users.py` avec une meilleure validation : + +```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""" + # Vérifier les doublons + 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. Route enrichie avec gestion d'erreurs + +Mettez à jour `src/api/routes/users.py` avec une meilleure gestion des erreurs : + +```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) + # Retourner l'utilisateur sans son hash de mot de passe + 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()) +``` + +## Ajouter plusieurs routes + +Vous pouvez ajouter plusieurs routes pour construire une API complète : + +
+ +```console +# Ajouter d'autres routes de ressources (nom de route d'abord, répertoire du projet ensuite) +$ fastkit addroute products my-awesome-api +$ fastkit addroute orders my-awesome-api +$ fastkit addroute categories my-awesome-api + +# Chaque commande crée une structure CRUD complète +``` + +
+ +Cela crée une API complète avec : + +- `/api/v1/users/` — gestion des utilisateurs +- `/api/v1/products/` — catalogue de produits +- `/api/v1/orders/` — traitement des commandes +- `/api/v1/categories/` — gestion des catégories + +## Organisation des routes + +### Regrouper des points d'extrémité liés + +Vous pouvez organiser les routes par domaine : + +```python +# src/api/api.py +from fastapi import APIRouter +from src.api.routes import users, products, orders, categories + +api_router = APIRouter() + +# Gestion des utilisateurs +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"] +) +``` + +### Ajouter des dépendances de route + +Ajoutez l'authentification ou d'autres dépendances : + +```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) +``` + +## Bonnes pratiques + +### 1. Nommage cohérent + +Suivez des conventions de nommage cohérentes : + +- **Noms de routes** : utilisez des noms au pluriel (`users`, `products`, `orders`) +- **Noms de schémas** : utilisez le singulier (`User`, `Product`, `Order`) +- **Classes CRUD** : terminez par `CRUD` (`UsersCRUD`, `ProductsCRUD`) + +### 2. Gestion des erreurs + +Gérez toujours les erreurs proprement : + +```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. Documentation + +Ajoutez des docstrings complètes : + +```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. Tests + +Testez toujours vos nouvelles routes : + +```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 +``` + +## Dépannage + +### La route n'apparaît pas + +Si votre route n'apparaît pas dans la documentation de l'API : + +1. **Vérifiez l'enregistrement du routeur** dans `src/api/api.py` +2. **Redémarrez le serveur** après avoir ajouté des routes +3. **Recherchez d'éventuelles erreurs d'import** dans le fichier de route + +### Erreurs d'import + +Si vous obtenez des erreurs d'import : + +1. **Vérifiez que la structure des fichiers** correspond à la disposition attendue +2. **Vérifiez les imports des schémas** dans les fichiers de route et de CRUD +3. **Assurez-vous que tous les fichiers `__init__.py` existent** + +### Le serveur ne démarre pas + +Si le serveur ne démarre pas après l'ajout de routes : + +1. **Recherchez les erreurs de syntaxe** dans les fichiers générés +2. **Vérifiez la compatibilité des schémas** entre les fichiers +3. **Consultez les journaux** pour des messages d'erreur précis + +## Étapes suivantes + +Maintenant que vous savez ajouter des routes : + +1. **[Votre premier projet](../tutorial/first-project.md)** : construire une API de blog complète +2. **[Référence CLI](cli-reference.md)** : découvrir toutes les commandes disponibles +3. **[Utiliser les modèles](using-templates.md)** : explorer les modèles de projet prêts à l'emploi + +!!! tip "Astuces de développement de routes" + - Testez toujours les nouvelles routes dans la documentation interactive (`/docs`) + - Utilisez des codes d'état HTTP pertinents + - Implémentez une gestion d'erreurs correcte pour tous les points d'extrémité + - Gardez les gestionnaires de route simples et déléguez la logique métier aux classes CRUD diff --git a/docs/fr/user-guide/choosing-a-starter.md b/docs/fr/user-guide/choosing-a-starter.md new file mode 100644 index 0000000..7a0985f --- /dev/null +++ b/docs/fr/user-guide/choosing-a-starter.md @@ -0,0 +1,145 @@ +# Quel starter choisir ? + +FastAPI-fastkit propose plusieurs façons d'amorcer un projet. Cette page est une **aide à la décision** pour les nouveaux venus : choisissez un chemin ici, puis allez sur [Démarrage rapide](quick-start.md) pour créer concrètement le projet. + +Si vous hésitez, la réponse courte est : + +> **Commencez par `fastkit init --interactive` et sélectionnez le préréglage `domain-starter`.** C'est l'option recommandée pour les projets d'API modernes. + +Le reste de cette page explique pourquoi, et quand opter pour autre chose. + +## TL;DR — choisir selon le profil d'utilisateur + +| Vous êtes... | Commencez avec | +|---|---| +| Nouveau sur FastAPI, vous voulez une démonstration guidée | `fastkit init --interactive` (préréglage : **`domain-starter`**) | +| Vous voulez une démo CRUD fonctionnelle à lire et modifier | `fastkit startdemo fastapi-default` | +| Vous voulez le squelette le plus minimal possible | `fastkit init --interactive` (préréglage : **`minimal`**) | +| Vous écrivez un prototype rapide ou un script monofichier | `fastkit init --interactive` (préréglage : **`single-module`**) | +| Vous avez besoin d'une vraie base de données (PostgreSQL + SQLAlchemy + Alembic) | `fastkit startdemo fastapi-psql-orm` | +| Vous voulez une disposition orientée domaine pour une API de taille moyenne | `fastkit init --interactive` (préréglage : **`domain-starter`**) | + +## `startdemo` vs `init --interactive` — quelle différence ? + +Ce sont les deux points d'entrée principaux. Ils répondent à des besoins différents. + +### `fastkit startdemo